Skip to content

Commit fe3dfc8

Browse files
committed
feat(plan): add task status indicators and UI enhancements
#331 - Introduced `PlanTask` class with status tracking (COMPLETED, FAILED, IN_PROGRESS, TODO). - Updated UI to display task status with icons and context menu for status changes. - Enhanced markdown parsing to support status indicators in plan tasks.
1 parent 0f463b7 commit fe3dfc8

File tree

8 files changed

+260
-81
lines changed

8 files changed

+260
-81
lines changed

core/src/main/kotlin/cc/unitmesh/devti/observer/agent/PlanList.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,3 @@ data class PlanList(
88
val planTasks: List<PlanTask>,
99
val completed: Boolean = false
1010
)
11-
12-
@Serializable
13-
data class PlanTask(val description: String, var completed: Boolean = false) {
14-
companion object {
15-
fun fromText(taskText: String): PlanTask {
16-
val isCompleted = taskText.contains("") ||
17-
Regex("\\[\\s*([xX])\\s*\\]").containsMatchIn(taskText)
18-
19-
val cleanedDescription = taskText
20-
.replace("", "")
21-
.replace(Regex("\\[\\s*[xX]\\s*\\]"), "[ ]")
22-
.replace(Regex("\\[\\s*\\]"), "")
23-
.trim()
24-
25-
return PlanTask(cleanedDescription, isCompleted)
26-
}
27-
}
28-
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cc.unitmesh.devti.observer.agent
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* 计划任务,描述了一个具体任务的细节和状态
7+
* @property description 任务描述
8+
* @property completed 任务是否已完成
9+
* @property status 任务状态(COMPLETED, FAILED, IN_PROGRESS, TODO)
10+
*/
11+
@Serializable
12+
class PlanTask(
13+
val description: String,
14+
var completed: Boolean = false,
15+
var status: TaskStatus = TaskStatus.TODO
16+
) {
17+
companion object {
18+
private val COMPLETED_PATTERN = Regex("^\\[(✓|x|X)\\]\\s*(.*)")
19+
private val FAILED_PATTERN = Regex("^\\[!\\]\\s*(.*)")
20+
private val IN_PROGRESS_PATTERN = Regex("^\\[\\*\\]\\s*(.*)")
21+
private val TODO_PATTERN = Regex("^\\[\\s*\\]\\s*(.*)")
22+
23+
/**
24+
* 从文本创建计划任务
25+
* @param text 任务文本,可能包含状态标记如 [✓]、[!]、[*] 或 [ ]
26+
* @return 创建的计划任务对象
27+
*/
28+
fun fromText(text: String): PlanTask {
29+
return when {
30+
COMPLETED_PATTERN.matches(text) -> {
31+
val description = COMPLETED_PATTERN.find(text)?.groupValues?.get(2) ?: text
32+
PlanTask(description, true, TaskStatus.COMPLETED)
33+
}
34+
FAILED_PATTERN.matches(text) -> {
35+
val description = FAILED_PATTERN.find(text)?.groupValues?.get(1) ?: text
36+
PlanTask(description, false, TaskStatus.FAILED)
37+
}
38+
IN_PROGRESS_PATTERN.matches(text) -> {
39+
val description = IN_PROGRESS_PATTERN.find(text)?.groupValues?.get(1) ?: text
40+
PlanTask(description, false, TaskStatus.IN_PROGRESS)
41+
}
42+
TODO_PATTERN.matches(text) -> {
43+
val description = TODO_PATTERN.find(text)?.groupValues?.get(1) ?: text
44+
PlanTask(description, false, TaskStatus.TODO)
45+
}
46+
else -> PlanTask(text)
47+
}
48+
}
49+
}
50+
51+
/**
52+
* 将任务转换为标准格式的文本表示
53+
* @return 包含状态标记的文本,如 [✓] Task description
54+
*/
55+
fun toText(): String {
56+
val statusMarker = when (status) {
57+
TaskStatus.COMPLETED -> "[✓]"
58+
TaskStatus.FAILED -> "[!]"
59+
TaskStatus.IN_PROGRESS -> "[*]"
60+
TaskStatus.TODO -> "[ ]"
61+
}
62+
return "$statusMarker $description"
63+
}
64+
65+
/**
66+
* 更新任务状态
67+
* @param newStatus 新的任务状态
68+
*/
69+
fun updateStatus(newStatus: TaskStatus) {
70+
status = newStatus
71+
completed = (status == TaskStatus.COMPLETED)
72+
}
73+
}
74+
75+
/**
76+
* 任务状态枚举
77+
*/
78+
@Serializable
79+
enum class TaskStatus {
80+
COMPLETED,
81+
FAILED,
82+
IN_PROGRESS,
83+
TODO
84+
}

core/src/main/kotlin/cc/unitmesh/devti/sketch/ui/ThoughtPlanSketchProvider.kt

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import cc.unitmesh.devti.gui.AutoDevToolWindowFactory
55
import cc.unitmesh.devti.gui.chat.message.ChatActionType
66
import cc.unitmesh.devti.observer.agent.AgentStateService
77
import cc.unitmesh.devti.observer.agent.PlanList
8+
import cc.unitmesh.devti.observer.agent.PlanTask
9+
import cc.unitmesh.devti.observer.agent.TaskStatus
810
import cc.unitmesh.devti.observer.plan.PlanBoard
911
import cc.unitmesh.devti.sketch.ui.code.CodeHighlightSketch
1012
import cc.unitmesh.devti.sketch.ui.plan.MarkdownPlanParser
@@ -119,18 +121,34 @@ class PlanSketch(
119121
border = JBUI.Borders.empty()
120122
}
121123

122-
val checkbox = JBCheckBox().apply {
123-
isSelected = task.completed
124-
addActionListener {
125-
task.completed = isSelected
124+
// First create task label with appropriate styling based on status
125+
val taskLabel = createStyledTaskLabel(task)
126+
127+
// Create a custom status indicator based on task status
128+
val statusIcon = when (task.status) {
129+
TaskStatus.COMPLETED -> JLabel(AllIcons.Actions.Checked)
130+
TaskStatus.FAILED -> JLabel(AllIcons.General.Error)
131+
TaskStatus.IN_PROGRESS -> JLabel(AllIcons.Actions.Execute)
132+
TaskStatus.TODO -> JBCheckBox().apply {
133+
isSelected = task.completed
134+
addActionListener {
135+
task.completed = isSelected
136+
if (isSelected) {
137+
task.updateStatus(TaskStatus.COMPLETED)
138+
} else {
139+
task.updateStatus(TaskStatus.TODO)
140+
}
141+
updateTaskLabel(taskLabel, task)
142+
}
143+
isBorderPainted = false
144+
isContentAreaFilled = false
126145
}
127-
isBorderPainted = false
128-
isContentAreaFilled = false
129146
}
147+
148+
taskPanel.add(statusIcon)
130149

131-
taskPanel.add(checkbox)
132-
133-
if (!task.completed) {
150+
// Add execute button for incomplete tasks
151+
if (task.status == TaskStatus.TODO || task.status == TaskStatus.IN_PROGRESS) {
134152
val executeButton = JButton(AllIcons.Actions.Execute).apply {
135153
border = BorderFactory.createEmptyBorder()
136154
preferredSize = Dimension(24, 24)
@@ -145,23 +163,51 @@ class PlanSketch(
145163

146164
taskPanel.add(executeButton)
147165
}
148-
149-
val taskLabel = JLabel(if (task.completed)
150-
"<html><strike>${task.description}</strike></html>"
151-
else task.description
152-
).apply {
153-
border = JBUI.Borders.emptyLeft(5)
166+
167+
taskPanel.add(taskLabel)
168+
169+
// Add context menu for changing task status
170+
val taskPopupMenu = JPopupMenu()
171+
val markCompletedItem = JMenuItem("Mark as Completed [✓]")
172+
val markInProgressItem = JMenuItem("Mark as In Progress [*]")
173+
val markFailedItem = JMenuItem("Mark as Failed [!]")
174+
val markTodoItem = JMenuItem("Mark as Todo [ ]")
175+
176+
markCompletedItem.addActionListener {
177+
task.updateStatus(TaskStatus.COMPLETED)
178+
updateTaskLabel(taskLabel, task)
179+
contentPanel.revalidate()
180+
contentPanel.repaint()
154181
}
155182

156-
checkbox.addActionListener {
157-
taskLabel.text = if (checkbox.isSelected)
158-
"<html><strike>${task.description}</strike></html>"
159-
else
160-
task.description
183+
markInProgressItem.addActionListener {
184+
task.updateStatus(TaskStatus.IN_PROGRESS)
185+
updateTaskLabel(taskLabel, task)
186+
contentPanel.revalidate()
187+
contentPanel.repaint()
161188
}
162189

163-
taskPanel.add(taskLabel)
164-
190+
markFailedItem.addActionListener {
191+
task.updateStatus(TaskStatus.FAILED)
192+
updateTaskLabel(taskLabel, task)
193+
contentPanel.revalidate()
194+
contentPanel.repaint()
195+
}
196+
197+
markTodoItem.addActionListener {
198+
task.updateStatus(TaskStatus.TODO)
199+
updateTaskLabel(taskLabel, task)
200+
contentPanel.revalidate()
201+
contentPanel.repaint()
202+
}
203+
204+
taskPopupMenu.add(markCompletedItem)
205+
taskPopupMenu.add(markInProgressItem)
206+
taskPopupMenu.add(markFailedItem)
207+
taskPopupMenu.add(markTodoItem)
208+
209+
taskLabel.componentPopupMenu = taskPopupMenu
210+
165211
contentPanel.add(taskPanel)
166212
}
167213

@@ -171,6 +217,30 @@ class PlanSketch(
171217
}
172218
}
173219

220+
// Helper method to create a styled task label based on status
221+
private fun createStyledTaskLabel(task: PlanTask): JLabel {
222+
val labelText = when (task.status) {
223+
TaskStatus.COMPLETED -> "<html><strike>${task.description}</strike></html>"
224+
TaskStatus.FAILED -> "<html><span style='color:red'>${task.description}</span></html>"
225+
TaskStatus.IN_PROGRESS -> "<html><span style='color:blue;font-style:italic'>${task.description}</span></html>"
226+
TaskStatus.TODO -> task.description
227+
}
228+
229+
return JLabel(labelText).apply {
230+
border = JBUI.Borders.emptyLeft(5)
231+
}
232+
}
233+
234+
// Helper method to update the task label based on current status
235+
private fun updateTaskLabel(label: JLabel, task: PlanTask) {
236+
label.text = when (task.status) {
237+
TaskStatus.COMPLETED -> "<html><strike>${task.description}</strike></html>"
238+
TaskStatus.FAILED -> "<html><span style='color:red'>${task.description}</span></html>"
239+
TaskStatus.IN_PROGRESS -> "<html><span style='color:blue;font-style:italic'>${task.description}</span></html>"
240+
TaskStatus.TODO -> task.description
241+
}
242+
}
243+
174244
override fun getExtensionName(): String = "ThoughtPlan"
175245

176246
override fun getViewText(): String = content
@@ -186,11 +256,12 @@ class PlanSketch(
186256

187257
fun updatePlan(newPlanItems: List<PlanList>) {
188258
if (newPlanItems.isNotEmpty()) {
189-
val completionState = mutableMapOf<String, Boolean>()
259+
// Save current states of all tasks
260+
val taskStateMap = mutableMapOf<String, Pair<Boolean, TaskStatus>>()
190261

191262
planLists.forEach { planItem ->
192263
planItem.planTasks.forEach { task ->
193-
completionState[task.description] = task.completed
264+
taskStateMap[task.description] = Pair(task.completed, task.status)
194265
}
195266
}
196267

@@ -201,9 +272,10 @@ class PlanSketch(
201272
planLists.add(newItem)
202273

203274
newItem.planTasks.forEach { task ->
204-
val savedCompletionState = completionState[task.description]
205-
if (savedCompletionState != null) {
206-
task.completed = savedCompletionState
275+
// Restore saved states if available
276+
taskStateMap[task.description]?.let { (completed, status) ->
277+
task.completed = completed
278+
task.status = status
207279
}
208280
}
209281
}

core/src/main/kotlin/cc/unitmesh/devti/sketch/ui/plan/MarkdownPlanParser.kt

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@ import org.intellij.markdown.parser.MarkdownParser
1919
*
2020
* ```markdown
2121
* 1. 领域模型重构:
22-
* - 将BlogPost实体合并到Blog聚合根,建立完整的领域对象
23-
* - 添加领域行为方法(发布、审核、评论等)
22+
* - [✓] 将BlogPost实体合并到Blog聚合根,建立完整的领域对象
23+
* - [*] 添加领域行为方法(发布、审核、评论等)
24+
* - [!] 修复数据模型冲突
25+
* - [ ] 实现新增的领域服务
2426
* 2. 分层结构调整:
25-
* - 清理entity层冗余对象
27+
* - [ ] 清理entity层冗余对象
2628
* ```
2729
*/
2830
object MarkdownPlanParser {
2931
private val LOG = logger<MarkdownPlanParser>()
3032
private val ROOT_ELEMENT_TYPE = IElementType("ROOT")
3133
private val CHECKMARK = ""
32-
private val GITHUB_TODO_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX]?)\\s*\\]\\s*(.*)")
33-
private val GITHUB_TODO_CHECKED = listOf("x", "X")
34+
private val GITHUB_TODO_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX!*✓]?)\\s*\\]\\s*(.*)")
35+
private val GITHUB_TODO_COMPLETED = listOf("x", "X", "")
36+
private val GITHUB_TODO_FAILED = listOf("!")
37+
private val GITHUB_TODO_IN_PROGRESS = listOf("*")
3438

3539
/**
3640
* 解析markdown文本为计划项列表
@@ -199,13 +203,13 @@ object MarkdownPlanParser {
199203
// Extract the task text and preserve the checkbox status
200204
val checkState = githubTodoMatch.groupValues[1]
201205
val todoText = githubTodoMatch.groupValues[2].trim()
202-
val isCompleted = checkState in GITHUB_TODO_CHECKED
203206

204-
// Add the task with the proper completion marker
205-
val formattedTask = if (isCompleted) {
206-
"[$CHECKMARK] $todoText"
207-
} else {
208-
"[ ] $todoText"
207+
// Determine task status based on marker
208+
val formattedTask = when {
209+
checkState in GITHUB_TODO_COMPLETED -> "[✓] $todoText"
210+
checkState in GITHUB_TODO_FAILED -> "[!] $todoText"
211+
checkState in GITHUB_TODO_IN_PROGRESS -> "[*] $todoText"
212+
else -> "[ ] $todoText"
209213
}
210214

211215
itemsList.add(PlanTask.fromText(formattedTask))

core/src/main/resources/genius/en/code/plan.devin

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,25 @@ Here is the rule you should follow:
4141
1. Thoroughly review `<user.question>`. Create/update an initial plan that includes all the necessary steps to
4242
resolve `<user.question>`, using the recommended steps provided below, and incorporating any requirements from
4343
the `<user.question>`. Place your plan inside code-fence block which language is `plan`.
44-
2. Review the project’s codebase, examining not only its structure but also the specific implementation details, to
44+
2. Plan should use a markdown ordered list to describe tasks and an unordered list to outline the steps for each task.
45+
3. Review the project’s codebase, examining not only its structure but also the specific implementation details, to
4546
identify all segments that may contribute to or help resolve the issue described in `<user.question>`.
46-
3. If `<user.question>` describes an error, create a script to reproduce it and run the script to confirm the error.
47-
4. Propose and explain the necessary code modifications to resolve `<user.question>`, ensuring that edge cases are
47+
4. If `<user.question>` describes an error, create a script to reproduce it and run the script to confirm the error.
48+
5. Propose and explain the necessary code modifications to resolve `<user.question>`, ensuring that edge cases are
4849
properly handled. Analyze these modifications before implementing them.
49-
5. use `<devin></devin>` command and code-fence block to Edit the source code in the repo to resolve `<user.question>`.
5050
6. You should alwasy think verify or auto-test your code to ensure it is correct and meets the requirements of `<user.question>`.
51-
7. Each plan should use a markdown ordered list to describe tasks and an unordered list to outline the steps for each task.
51+
52+
For each step, document your reasoning process inside `plan` code block. Include:
53+
54+
1. Updated plan with task progress indicators (`[✓]` for completed, `[!]` for failed, `[*]` for in-progress).
55+
56+
For example:
57+
58+
```plan
59+
1. xxx
60+
- [ ] xxx
61+
2. xxx
62+
```
5263

5364
If `<user.question>` directly contradicts any of these steps, follow the instructions from `<user.question>`
5465
first. Be thorough in your thinking process, so it's okay if it is lengthy.

0 commit comments

Comments
 (0)