Skip to content

Commit fbb6993

Browse files
committed
feat(mcp): add result panel with tool call visualization
Introduce McpResultPanel to display LLM responses with tabbed view for raw output and parsed tool calls. Updates system prompt format and integrates panel into preview editor. Enhances tool integration visibility by parsing and displaying function calls in a structured way.
1 parent 0773cb6 commit fbb6993

File tree

3 files changed

+218
-21
lines changed

3 files changed

+218
-21
lines changed

core/src/main/kotlin/cc/unitmesh/devti/mcp/editor/McpLlmConfigDialog.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ data class McpLlmConfig(
2424
fun createSystemPrompt(): String {
2525
val systemPrompt = """
2626
In this environment you have access to a set of tools you can use to answer the user's question.
27-
You can invoke functions by writing a "<devins:function_calls>" block like the following as part of your reply to the user:
27+
You can invoke functions by writing a "<devins:function_calls>" inside markdown code-block like the following as part of your reply to the user:
28+
29+
```xml
2830
<devins:function_calls>
2931
<devins:invoke name="${'$'}FUNCTION_NAME">
3032
<devins:parameter name="${'$'}PARAMETER_NAME">${'$'}PARAMETER_VALUE</devins:parameter>
@@ -34,6 +36,7 @@ You can invoke functions by writing a "<devins:function_calls>" block like the f
3436
...
3537
</devins:invoke>
3638
</devins:function_calls>
39+
```
3740
3841
String and scalar parameters should be specified as is, while lists and objects should use JSON format.
3942

core/src/main/kotlin/cc/unitmesh/devti/mcp/editor/McpPreviewEditor.kt

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ open class McpPreviewEditor(
6161
private lateinit var chatInput: JBTextField
6262
private lateinit var testButton: ActionButton
6363
private lateinit var configButton: JButton
64-
private lateinit var resultTextArea: JTextArea
65-
private lateinit var resultPanel: JPanel
64+
private lateinit var resultPanel: McpResultPanel
6665
private val config = McpLlmConfig()
6766
private val borderColor = JBColor(0xE5E7EB, 0x3C3F41) // Equivalent to Tailwind gray-200
6867
private val textGray = JBColor(0x6B7280, 0x9DA0A8) // Equivalent to Tailwind gray-500
@@ -152,25 +151,11 @@ open class McpPreviewEditor(
152151
background = UIUtil.getPanelBackground()
153152
}
154153

155-
resultPanel = JPanel(BorderLayout()).apply {
154+
resultPanel = McpResultPanel().apply {
156155
background = UIUtil.getPanelBackground()
157156
isVisible = false
158157
}
159158

160-
resultTextArea = JTextArea().apply {
161-
isEditable = false
162-
wrapStyleWord = true
163-
lineWrap = true
164-
background = UIUtil.getPanelBackground()
165-
border = JBUI.Borders.empty(4)
166-
}
167-
168-
val resultScrollPane = JBScrollPane(resultTextArea).apply {
169-
border = BorderFactory.createEmptyBorder()
170-
background = UIUtil.getPanelBackground()
171-
}
172-
173-
resultPanel.add(resultScrollPane, BorderLayout.CENTER)
174159
toolsWrapper.add(toolsScrollPane, BorderLayout.CENTER)
175160

176161
val bottomPanel = BorderLayoutPanel().apply {
@@ -296,15 +281,15 @@ open class McpPreviewEditor(
296281
}
297282
}
298283

299-
private fun sendMessage() {
284+
fun sendMessage() {
300285
val llmConfig = LlmConfig.load().firstOrNull { it.name == chatbotSelector.selectedItem }
301286
?: LlmConfig.default()
302287
val llmProvider = CustomLLMProvider(project, llmConfig)
303288
val message = chatInput.text.trim()
304289
val result = StringBuilder()
305290
val stream: Flow<String> = llmProvider.stream(message, systemPrompt = config.createSystemPrompt())
306291

307-
resultTextArea.text = "Loading response..."
292+
resultPanel.setText("Loading response...")
308293
resultPanel.isVisible = true
309294
mainPanel.revalidate()
310295
mainPanel.repaint()
@@ -313,7 +298,7 @@ open class McpPreviewEditor(
313298
stream.cancellable().collect { chunk ->
314299
result.append(chunk)
315300
SwingUtilities.invokeLater {
316-
resultTextArea.text = result.toString()
301+
resultPanel.setText(result.toString())
317302
mainPanel.revalidate()
318303
mainPanel.repaint()
319304
}
@@ -343,3 +328,4 @@ open class McpPreviewEditor(
343328
loadingJob?.cancel()
344329
}
345330
}
331+
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package cc.unitmesh.devti.mcp.editor
2+
3+
import com.intellij.ui.JBColor
4+
import com.intellij.ui.components.JBLabel
5+
import com.intellij.ui.components.JBScrollPane
6+
import com.intellij.ui.components.JBTabbedPane
7+
import com.intellij.util.ui.JBUI
8+
import com.intellij.util.ui.UIUtil
9+
import java.awt.BorderLayout
10+
import java.awt.GridBagConstraints
11+
import java.awt.GridBagLayout
12+
import javax.swing.*
13+
import javax.swing.border.CompoundBorder
14+
import javax.swing.border.EmptyBorder
15+
import javax.swing.border.MatteBorder
16+
import java.util.regex.Pattern
17+
18+
class McpResultPanel : JPanel(BorderLayout()) {
19+
private val rawResultTextArea = JTextArea().apply {
20+
isEditable = false
21+
wrapStyleWord = true
22+
lineWrap = true
23+
background = UIUtil.getPanelBackground()
24+
border = JBUI.Borders.empty(4)
25+
}
26+
27+
private val toolsPanel = JPanel(GridBagLayout()).apply {
28+
background = UIUtil.getPanelBackground()
29+
border = JBUI.Borders.empty(8)
30+
}
31+
32+
private val tabbedPane = JBTabbedPane().apply {
33+
addTab("Response", JBScrollPane(rawResultTextArea).apply {
34+
border = BorderFactory.createEmptyBorder()
35+
})
36+
37+
addTab("Tools", JBScrollPane(toolsPanel).apply {
38+
border = BorderFactory.createEmptyBorder()
39+
})
40+
}
41+
42+
private val borderColor = JBColor(0xE5E7EB, 0x3C3F41) // Equivalent to Tailwind gray-200
43+
44+
init {
45+
background = UIUtil.getPanelBackground()
46+
add(tabbedPane, BorderLayout.CENTER)
47+
}
48+
49+
fun setText(text: String) {
50+
rawResultTextArea.text = text
51+
parseAndShowTools(text)
52+
}
53+
54+
private fun parseAndShowTools(text: String) {
55+
toolsPanel.removeAll()
56+
57+
val toolCalls = extractToolCalls(text)
58+
if (toolCalls.isEmpty()) {
59+
val noToolsLabel = JBLabel("No tool calls found in the response").apply {
60+
foreground = JBColor(0x6B7280, 0x9DA0A8) // Gray text
61+
horizontalAlignment = SwingConstants.CENTER
62+
}
63+
64+
val gbc = GridBagConstraints().apply {
65+
gridx = 0
66+
gridy = 0
67+
weightx = 1.0
68+
weighty = 1.0
69+
fill = GridBagConstraints.BOTH
70+
}
71+
toolsPanel.add(noToolsLabel, gbc)
72+
} else {
73+
var gridY = 0
74+
75+
toolCalls.forEach { toolCall ->
76+
val toolPanel = createToolCallPanel(toolCall)
77+
78+
val gbc = GridBagConstraints().apply {
79+
gridx = 0
80+
gridy = gridY++
81+
weightx = 1.0
82+
fill = GridBagConstraints.HORIZONTAL
83+
insets = JBUI.insets(0, 0, 10, 0)
84+
}
85+
86+
toolsPanel.add(toolPanel, gbc)
87+
}
88+
89+
// Add empty filler panel at the end
90+
val fillerPanel = JPanel()
91+
fillerPanel.isOpaque = false
92+
93+
val gbc = GridBagConstraints().apply {
94+
gridx = 0
95+
gridy = gridY
96+
weightx = 1.0
97+
weighty = 1.0
98+
fill = GridBagConstraints.BOTH
99+
}
100+
101+
toolsPanel.add(fillerPanel, gbc)
102+
}
103+
104+
toolsPanel.revalidate()
105+
toolsPanel.repaint()
106+
107+
if (toolCalls.isNotEmpty()) {
108+
tabbedPane.selectedIndex = 1
109+
}
110+
}
111+
112+
private fun extractToolCalls(text: String): List<ToolCall> {
113+
val toolCalls = mutableListOf<ToolCall>()
114+
115+
val codeBlockPattern = Pattern.compile("```(?:xml|)\\s*devins:function_calls\\s*(.*?)\\s*```", Pattern.DOTALL)
116+
val matcher = codeBlockPattern.matcher(text)
117+
118+
while (matcher.find()) {
119+
val xmlContent = matcher.group(1)
120+
121+
val invokePattern = Pattern.compile("<devins:invoke\\s+name=\"([^\"]+)\">\\s*(.*?)\\s*</devins:invoke>", Pattern.DOTALL)
122+
val invokeMatcher = invokePattern.matcher(xmlContent)
123+
124+
while (invokeMatcher.find()) {
125+
val toolName = invokeMatcher.group(1)
126+
val paramsXml = invokeMatcher.group(2)
127+
128+
val params = mutableMapOf<String, String>()
129+
val paramPattern = Pattern.compile("<devins:parameter\\s+name=\"([^\"]+)\">(.*?)</devins:parameter>", Pattern.DOTALL)
130+
val paramMatcher = paramPattern.matcher(paramsXml)
131+
132+
while (paramMatcher.find()) {
133+
val paramName = paramMatcher.group(1)
134+
val paramValue = paramMatcher.group(2)
135+
params[paramName] = paramValue
136+
}
137+
138+
toolCalls.add(ToolCall(toolName, params))
139+
}
140+
}
141+
142+
return toolCalls
143+
}
144+
145+
private fun createToolCallPanel(toolCall: ToolCall): JPanel {
146+
val panel = JPanel(BorderLayout()).apply {
147+
background = UIUtil.getPanelBackground().brighter()
148+
border = CompoundBorder(
149+
MatteBorder(1, 1, 1, 1, borderColor),
150+
EmptyBorder(JBUI.insets(10))
151+
)
152+
}
153+
154+
val titleLabel = JBLabel(toolCall.name).apply {
155+
font = JBUI.Fonts.label(14f).asBold()
156+
border = JBUI.Borders.emptyBottom(8)
157+
}
158+
159+
val paramsPanel = JPanel(GridBagLayout()).apply {
160+
isOpaque = false
161+
}
162+
163+
var paramGridY = 0
164+
toolCall.parameters.forEach { (name, value) ->
165+
val nameLabel = JBLabel(name + ":").apply {
166+
font = JBUI.Fonts.label(12f).asBold()
167+
border = JBUI.Borders.emptyRight(8)
168+
}
169+
170+
val valueLabel = JTextArea(value).apply {
171+
isEditable = false
172+
wrapStyleWord = true
173+
lineWrap = true
174+
background = UIUtil.getPanelBackground().brighter()
175+
border = null
176+
margin = JBUI.insets(0)
177+
}
178+
179+
val nameGbc = GridBagConstraints().apply {
180+
gridx = 0
181+
gridy = paramGridY
182+
anchor = GridBagConstraints.NORTHWEST
183+
insets = JBUI.insets(2)
184+
}
185+
186+
val valueGbc = GridBagConstraints().apply {
187+
gridx = 1
188+
gridy = paramGridY++
189+
weightx = 1.0
190+
fill = GridBagConstraints.HORIZONTAL
191+
insets = JBUI.insets(2)
192+
}
193+
194+
paramsPanel.add(nameLabel, nameGbc)
195+
paramsPanel.add(valueLabel, valueGbc)
196+
}
197+
198+
panel.add(titleLabel, BorderLayout.NORTH)
199+
panel.add(paramsPanel, BorderLayout.CENTER)
200+
201+
return panel
202+
}
203+
204+
data class ToolCall(
205+
val name: String,
206+
val parameters: Map<String, String>
207+
)
208+
}

0 commit comments

Comments
 (0)