Skip to content

Commit 934a985

Browse files
committed
feat(plan): add plan review action and test cases #259
- Add `PlanReviewAction` for reviewing plans and removing Markdown code blocks. - Include `PlanReviewActionTest` to verify code block removal functionality. - Update `MarkdownPlanParser` to support formatting plans as Markdown. - Rename `allMessages` to `getAllMessages` for consistency.
1 parent 2d1d341 commit 934a985

File tree

7 files changed

+215
-6
lines changed

7 files changed

+215
-6
lines changed

core/src/233/main/resources/META-INF/autodev-core.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,5 +417,11 @@
417417
<reference id="cc.unitmesh.devti.QuickAssistant"/>
418418
<reference id="cc.unitmesh.devti.EditSettings"/>
419419
</group>
420+
421+
<group id="AutoDevPlanner.ToolWindow.TitleActions">
422+
<action id="ReviewPlan" icon="AllIcons.General.InspectionsEye"
423+
class="cc.unitmesh.devti.observer.plan.PlanReviewAction"/>
424+
<separator/>
425+
</group>
420426
</actions>
421427
</idea-plugin>

core/src/main/kotlin/cc/unitmesh/devti/gui/AutoDevPlanerToolWindowFactory.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import cc.unitmesh.devti.observer.plan.MarkdownPlanParser
66
import cc.unitmesh.devti.observer.plan.PlanUpdateListener
77
import cc.unitmesh.devti.sketch.ui.plan.PlanSketch
88
import com.intellij.openapi.Disposable
9+
import com.intellij.openapi.actionSystem.ex.ActionUtil
910
import com.intellij.openapi.application.ApplicationManager
1011
import com.intellij.openapi.project.DumbAware
1112
import com.intellij.openapi.project.Project
@@ -33,6 +34,7 @@ class AutoDevPlanerToolWindowFactory : ToolWindowFactory, ToolWindowManagerListe
3334
val manager = toolWindow.contentManager
3435
manager.addContent(manager.factory.createContent(panel, null, false).apply { isCloseable = false })
3536
project.messageBus.connect(manager).subscribe(ToolWindowManagerListener.TOPIC, this)
37+
toolWindow.setTitleActions(listOfNotNull(ActionUtil.getAction("AutoDevPlanner.ToolWindow.TitleActions")))
3638
}
3739

3840
override fun stateChanged(manager: ToolWindowManager) {

core/src/main/kotlin/cc/unitmesh/devti/gui/toolbar/CopyAllMessagesAction.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class CopyAllMessagesAction : AnAction("Copy All Messages", "Copy all messages",
4747

4848
private fun copyMessages(project: Project?) {
4949
val agentStateService = project?.getService(AgentStateService::class.java) ?: return
50-
var allText = agentStateService.allMessages().joinToString("\n") { it.content }
50+
var allText = agentStateService.getAllMessages().joinToString("\n") { it.content }
5151
val selection = StringSelection(allText)
5252
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
5353
clipboard.setContents(selection, null)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AgentStateService {
4747
return intention
4848
}
4949

50-
fun allMessages(): List<Message> {
50+
fun getAllMessages(): List<Message> {
5151
return state.messages
5252
}
5353

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,9 @@ object MarkdownPlanParser {
212212
MarkdownElementTypes.ORDERED_LIST -> {
213213
processOrderedList(node, planSections)
214214
// 跳过递归以避免重复处理
215-
}
215+
}
216216

217-
MarkdownElementTypes.UNORDERED_LIST -> {
217+
MarkdownElementTypes.UNORDERED_LIST -> {
218218
processUnorderedList(node)
219219
// 跳过递归以避免重复处理
220220
}
@@ -249,7 +249,7 @@ object MarkdownPlanParser {
249249

250250
// 处理子任务列表
251251
item.children.forEach { childNode ->
252-
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
252+
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
253253
processUnorderedList(childNode)
254254
}
255255
}
@@ -326,7 +326,7 @@ object MarkdownPlanParser {
326326

327327
// 处理嵌套任务
328328
item.children.forEach { childNode ->
329-
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
329+
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
330330
extractTasks(childNode, taskList)
331331
}
332332
}
@@ -357,4 +357,14 @@ object MarkdownPlanParser {
357357

358358
// 为保持向后兼容性,提供parse方法的别名
359359
fun parse(content: String): List<AgentTaskEntry> = interpretPlan(content)
360+
fun formatPlanToMarkdown(entries: MutableList<AgentTaskEntry>): String {
361+
val stringBuilder = StringBuilder()
362+
entries.forEachIndexed { index, entry ->
363+
stringBuilder.append("${index + 1}. ${entry.title}\n")
364+
entry.steps.forEach { step ->
365+
stringBuilder.append(" - [${if (step.completed) "x" else " "}] ${step.step}\n")
366+
}
367+
}
368+
return stringBuilder.toString()
369+
}
360370
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// filepath: /Volumes/source/ai/autocrud/core/src/main/kotlin/cc/unitmesh/devti/observer/plan/PlanReviewAction.kt
2+
package cc.unitmesh.devti.observer.plan
3+
4+
import cc.unitmesh.devti.observer.agent.AgentStateService
5+
import com.intellij.openapi.actionSystem.ActionUpdateThread
6+
import com.intellij.openapi.actionSystem.AnAction
7+
import com.intellij.openapi.actionSystem.AnActionEvent
8+
import org.intellij.markdown.IElementType
9+
import org.intellij.markdown.MarkdownElementTypes
10+
import org.intellij.markdown.ast.ASTNode
11+
import org.intellij.markdown.ast.accept
12+
import org.intellij.markdown.ast.getTextInNode
13+
import org.intellij.markdown.ast.visitors.RecursiveVisitor
14+
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
15+
import org.intellij.markdown.parser.MarkdownParser
16+
17+
class PlanReviewAction : AnAction() {
18+
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
19+
20+
override fun actionPerformed(anActionEvent: AnActionEvent) {
21+
val project = anActionEvent.project ?: return
22+
val agentStateService = project.getService(AgentStateService::class.java)
23+
24+
val currentPlan = agentStateService.getPlan()
25+
val plan = MarkdownPlanParser.formatPlanToMarkdown(currentPlan)
26+
27+
/// call llm to evaluate the plan
28+
val allMessages = agentStateService.getAllMessages()
29+
val withOutCode = allMessages.map {
30+
removeAllMarkdownCode(it.content)
31+
}
32+
}
33+
}
34+
35+
fun removeAllMarkdownCode(markdownContent: String): String {
36+
if (markdownContent.isEmpty()) return markdownContent
37+
38+
val flavour = GFMFlavourDescriptor()
39+
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(markdownContent)
40+
41+
val codeBlockReplacements = mutableListOf<Pair<IntRange, String>>()
42+
43+
parsedTree.accept(object : RecursiveVisitor() {
44+
override fun visitNode(node: ASTNode) {
45+
if (node.type == MarkdownElementTypes.CODE_FENCE) {
46+
val language = extractCodeFenceLanguage(node, markdownContent)
47+
val replacement = "```$language\n// you can skip this part of the code.\n```"
48+
codeBlockReplacements.add(node.startOffset..node.endOffset to replacement)
49+
} else if (node.type == MarkdownElementTypes.CODE_BLOCK) {
50+
val replacement = "```\n// you can skip this part of the code.\n```"
51+
codeBlockReplacements.add(node.startOffset..node.endOffset to replacement)
52+
} else {
53+
super.visitNode(node)
54+
}
55+
}
56+
})
57+
58+
codeBlockReplacements.sortByDescending { it.first.first }
59+
60+
val result = StringBuilder(markdownContent)
61+
for ((range, replacement) in codeBlockReplacements) {
62+
result.replace(range.first, range.last, replacement)
63+
}
64+
65+
return result.toString()
66+
}
67+
68+
private fun extractCodeFenceLanguage(node: ASTNode, markdownContent: String): String {
69+
val nodeText = node.getTextInNode(markdownContent).toString()
70+
val firstLine = nodeText.lines().firstOrNull() ?: ""
71+
72+
val languageMatch = Regex("^```(.*)$").find(firstLine.trim())
73+
return languageMatch?.groupValues?.getOrNull(1)?.trim() ?: ""
74+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package cc.unitmesh.devti.observer.plan
2+
3+
import org.junit.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertTrue
6+
7+
class PlanReviewActionTest {
8+
@Test
9+
fun `test removeAllMarkdownCode with empty content`() {
10+
val result = removeAllMarkdownCode("")
11+
assertEquals("", result)
12+
}
13+
14+
@Test
15+
fun `test removeAllMarkdownCode with no code blocks`() {
16+
val content = """
17+
# Title
18+
This is a paragraph.
19+
* List item
20+
""".trimIndent()
21+
22+
val result = removeAllMarkdownCode(content)
23+
assertEquals(content, result)
24+
}
25+
26+
@Test
27+
fun `test removeAllMarkdownCode with one code fence`() {
28+
val content = """
29+
# Title
30+
This is a paragraph.
31+
32+
```kotlin
33+
val x = 10
34+
```
35+
36+
More text.
37+
""".trimIndent()
38+
39+
val expected = """
40+
# Title
41+
This is a paragraph.
42+
43+
```kotlin
44+
// you can skip this part of the code.
45+
```
46+
47+
More text.
48+
""".trimIndent()
49+
50+
val result = removeAllMarkdownCode(content)
51+
assertEquals(expected, result)
52+
}
53+
54+
@Test
55+
fun `test removeAllMarkdownCode with multiple code blocks`() {
56+
val content = """
57+
# Title
58+
59+
```kotlin
60+
val x = 10
61+
```
62+
63+
Some text.
64+
65+
```java
66+
int y = 20;
67+
```
68+
69+
More text.
70+
""".trimIndent()
71+
72+
val expected = """
73+
# Title
74+
75+
```kotlin
76+
// you can skip this part of the code.
77+
```
78+
79+
Some text.
80+
81+
```java
82+
// you can skip this part of the code.
83+
```
84+
85+
More text.
86+
""".trimIndent()
87+
88+
val result = removeAllMarkdownCode(content)
89+
assertEquals(expected, result)
90+
}
91+
92+
@Test
93+
fun `test removeAllMarkdownCode with indented code block`() {
94+
val content = """
95+
# Title
96+
97+
This is a code block
98+
More code
99+
100+
Text after.
101+
""".trimIndent()
102+
103+
val expected = """
104+
# Title
105+
106+
```
107+
// you can skip this part of the code.
108+
```
109+
110+
Text after.
111+
""".trimIndent()
112+
113+
val result = removeAllMarkdownCode(content)
114+
assertEquals(expected, result)
115+
}
116+
}
117+

0 commit comments

Comments
 (0)