Skip to content

Commit a3d0400

Browse files
committed
feat(plan): add PlanSummaryBar component across all platforms
- Add PlanSummaryData model in mpp-core for lightweight UI summary - Add PlanSummaryBar Compose component in mpp-ui - Add IdeaPlanSummaryBar with Jewel styling for mpp-idea - Add JewelRenderer plan tracking (currentPlan StateFlow) - Add PlanSummaryBar React component for mpp-vscode - Integrate PlanSummaryBar above input box in all platforms The PlanSummaryBar displays a collapsible summary of the current plan above the input box, showing progress, current step, and allowing users to expand for full plan details.
1 parent 316c277 commit a3d0400

File tree

11 files changed

+1578
-2
lines changed

11 files changed

+1578
-2
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cc.unitmesh.agent.plan
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Lightweight summary data for displaying plan status in UI.
7+
*
8+
* This is a simplified view of AgentPlan optimized for UI display,
9+
* containing only the essential information needed for the summary bar.
10+
*/
11+
@Serializable
12+
data class PlanSummaryData(
13+
val planId: String,
14+
val title: String,
15+
val totalSteps: Int,
16+
val completedSteps: Int,
17+
val failedSteps: Int,
18+
val progressPercent: Int,
19+
val status: TaskStatus,
20+
val currentStepDescription: String?,
21+
val tasks: List<TaskSummary>
22+
) {
23+
companion object {
24+
/**
25+
* Create a PlanSummaryData from an AgentPlan
26+
*/
27+
fun from(plan: AgentPlan): PlanSummaryData {
28+
val allSteps = plan.tasks.flatMap { it.steps }
29+
val completedSteps = allSteps.count { it.status == TaskStatus.COMPLETED }
30+
val failedSteps = allSteps.count { it.status == TaskStatus.FAILED }
31+
32+
// Find current step (first in-progress or first todo)
33+
val currentStep = allSteps.firstOrNull { it.status == TaskStatus.IN_PROGRESS }
34+
?: allSteps.firstOrNull { it.status == TaskStatus.TODO }
35+
36+
// Title: use first task title or "Plan"
37+
val title = plan.tasks.firstOrNull()?.title ?: "Plan"
38+
39+
return PlanSummaryData(
40+
planId = plan.id,
41+
title = title,
42+
totalSteps = allSteps.size,
43+
completedSteps = completedSteps,
44+
failedSteps = failedSteps,
45+
progressPercent = plan.progressPercent,
46+
status = plan.status,
47+
currentStepDescription = currentStep?.description,
48+
tasks = plan.tasks.map { TaskSummary.from(it) }
49+
)
50+
}
51+
}
52+
}
53+
54+
/**
55+
* Summary of a single task within a plan
56+
*/
57+
@Serializable
58+
data class TaskSummary(
59+
val id: String,
60+
val title: String,
61+
val status: TaskStatus,
62+
val completedSteps: Int,
63+
val totalSteps: Int,
64+
val steps: List<StepSummary>
65+
) {
66+
companion object {
67+
fun from(task: PlanTask): TaskSummary {
68+
return TaskSummary(
69+
id = task.id,
70+
title = task.title,
71+
status = task.status,
72+
completedSteps = task.completedStepCount,
73+
totalSteps = task.totalStepCount,
74+
steps = task.steps.map { StepSummary.from(it) }
75+
)
76+
}
77+
}
78+
}
79+
80+
/**
81+
* Summary of a single step within a task
82+
*/
83+
@Serializable
84+
data class StepSummary(
85+
val id: String,
86+
val description: String,
87+
val status: TaskStatus
88+
) {
89+
companion object {
90+
fun from(step: PlanStep): StepSummary {
91+
return StepSummary(
92+
id = step.id,
93+
description = step.description,
94+
status = step.status
95+
)
96+
}
97+
}
98+
}

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package cc.unitmesh.devins.idea.renderer
22

3+
import cc.unitmesh.agent.plan.AgentPlan
4+
import cc.unitmesh.agent.plan.MarkdownPlanParser
5+
import cc.unitmesh.agent.plan.TaskStatus as PlanTaskStatus
36
import cc.unitmesh.agent.render.BaseRenderer
47
import cc.unitmesh.devins.llm.MessageRole
58
import cc.unitmesh.agent.render.RendererUtils
@@ -76,6 +79,10 @@ class JewelRenderer : BaseRenderer() {
7679
private val _tasks = MutableStateFlow<List<TaskInfo>>(emptyList())
7780
val tasks: StateFlow<List<TaskInfo>> = _tasks.asStateFlow()
7881

82+
// Plan tracking (from plan management tool)
83+
private val _currentPlan = MutableStateFlow<AgentPlan?>(null)
84+
val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow()
85+
7986
// BaseRenderer implementation
8087

8188
override fun renderIterationHeader(current: Int, max: Int) {
@@ -138,6 +145,11 @@ class JewelRenderer : BaseRenderer() {
138145
updateTaskFromToolCall(params)
139146
}
140147

148+
// Handle plan management tool - update plan state
149+
if (toolName == "plan") {
150+
updatePlanFromToolCall(params)
151+
}
152+
141153
// Skip adding ToolCallItem for Shell - will be replaced by LiveTerminalItem
142154
// This prevents the "two bubbles" problem
143155
if (toolType == ToolType.Shell) {
@@ -207,6 +219,54 @@ class JewelRenderer : BaseRenderer() {
207219
}
208220
}
209221

222+
/**
223+
* Update plan state from plan management tool call
224+
*/
225+
private fun updatePlanFromToolCall(params: Map<String, String>) {
226+
val action = params["action"]?.uppercase() ?: return
227+
val planMarkdown = params["planMarkdown"] ?: ""
228+
229+
when (action) {
230+
"CREATE", "UPDATE" -> {
231+
if (planMarkdown.isNotBlank()) {
232+
_currentPlan.value = MarkdownPlanParser.parseToPlan(planMarkdown)
233+
}
234+
}
235+
"COMPLETE_STEP" -> {
236+
val taskIndex = params["taskIndex"]?.toIntOrNull() ?: return
237+
val stepIndex = params["stepIndex"]?.toIntOrNull() ?: return
238+
_currentPlan.value?.let { plan ->
239+
if (taskIndex in 1..plan.tasks.size) {
240+
val task = plan.tasks[taskIndex - 1]
241+
if (stepIndex in 1..task.steps.size) {
242+
val step = task.steps[stepIndex - 1]
243+
step.complete()
244+
task.updateStatusFromSteps()
245+
// Trigger recomposition by creating a new plan instance
246+
_currentPlan.value = plan.copy(updatedAt = System.currentTimeMillis())
247+
}
248+
}
249+
}
250+
}
251+
"FAIL_STEP" -> {
252+
val taskIndex = params["taskIndex"]?.toIntOrNull() ?: return
253+
val stepIndex = params["stepIndex"]?.toIntOrNull() ?: return
254+
_currentPlan.value?.let { plan ->
255+
if (taskIndex in 1..plan.tasks.size) {
256+
val task = plan.tasks[taskIndex - 1]
257+
if (stepIndex in 1..task.steps.size) {
258+
val step = task.steps[stepIndex - 1]
259+
step.fail()
260+
task.updateStatusFromSteps()
261+
_currentPlan.value = plan.copy(updatedAt = System.currentTimeMillis())
262+
}
263+
}
264+
}
265+
}
266+
// VIEW action doesn't modify state
267+
}
268+
}
269+
210270
override fun renderToolResult(
211271
toolName: String,
212272
success: Boolean,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ fun IdeaAgentApp(
5151
val timeline by viewModel.renderer.timeline.collectAsState()
5252
val streamingOutput by viewModel.renderer.currentStreamingOutput.collectAsState()
5353
val isExecuting by viewModel.isExecuting.collectAsState()
54+
val currentPlan by viewModel.renderer.currentPlan.collectAsState()
5455
val showConfigDialog by viewModel.showConfigDialog.collectAsState()
5556
val mcpPreloadingMessage by viewModel.mcpPreloadingMessage.collectAsState()
5657
val configWrapper by viewModel.configWrapper.collectAsState()
@@ -172,6 +173,7 @@ fun IdeaAgentApp(
172173
onConfigSelect = { config ->
173174
viewModel.setActiveConfig(config.name)
174175
},
176+
currentPlan = currentPlan,
175177
onConfigureClick = { viewModel.setShowConfigDialog(true) }
176178
)
177179
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier
1111
import androidx.compose.ui.awt.SwingPanel
1212
import androidx.compose.ui.draw.clip
1313
import androidx.compose.ui.unit.dp
14+
import cc.unitmesh.agent.plan.AgentPlan
1415
import cc.unitmesh.devins.idea.editor.*
16+
import cc.unitmesh.devins.idea.toolwindow.plan.IdeaPlanSummaryBar
1517
import cc.unitmesh.llm.NamedModelConfig
1618
import com.intellij.openapi.Disposable
1719
import com.intellij.openapi.application.ApplicationManager
@@ -76,7 +78,8 @@ fun IdeaDevInInputArea(
7678
availableConfigs: List<NamedModelConfig> = emptyList(),
7779
currentConfigName: String? = null,
7880
onConfigSelect: (NamedModelConfig) -> Unit = {},
79-
onConfigureClick: () -> Unit = {}
81+
onConfigureClick: () -> Unit = {},
82+
currentPlan: AgentPlan? = null
8083
) {
8184
var inputText by remember { mutableStateOf("") }
8285
var devInInput by remember { mutableStateOf<IdeaDevInInput?>(null) }
@@ -102,6 +105,12 @@ fun IdeaDevInInputArea(
102105
shape = borderShape
103106
)
104107
) {
108+
// Plan summary bar - shown above top toolbar when a plan is active
109+
IdeaPlanSummaryBar(
110+
plan = currentPlan,
111+
modifier = Modifier.fillMaxWidth()
112+
)
113+
105114
// Top toolbar with file selection (no individual border)
106115
IdeaTopToolbar(
107116
project = project,

0 commit comments

Comments
 (0)