Skip to content

Commit 56e9609

Browse files
committed
feat(mcp): add message log panel for tool execution #371
- Introduce McpMessage model and MessageType enum - Add McpMessageLogPanel with table and detail view - Implement logging for tool requests/responses - Add "Execute All Tools" button and messages tab - Include i18n support for new UI elements
1 parent 28a4fa8 commit 56e9609

File tree

5 files changed

+256
-8
lines changed

5 files changed

+256
-8
lines changed

core/src/main/kotlin/cc/unitmesh/devti/mcp/ui/McpChatResultPanel.kt

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cc.unitmesh.devti.mcp.ui
33
import cc.unitmesh.devti.AutoDevBundle
44
import cc.unitmesh.devti.mcp.client.CustomMcpServerManager
55
import cc.unitmesh.devti.mcp.ui.model.McpChatConfig
6+
import cc.unitmesh.devti.mcp.ui.model.McpMessage
7+
import cc.unitmesh.devti.mcp.ui.model.MessageType
68
import cc.unitmesh.devti.mcp.ui.model.ToolCall
79
import com.intellij.openapi.project.Project
810
import com.intellij.ui.JBColor
@@ -15,6 +17,8 @@ import io.modelcontextprotocol.kotlin.sdk.Tool
1517
import kotlinx.serialization.encodeToString
1618
import kotlinx.serialization.json.Json
1719
import java.awt.*
20+
import java.time.LocalDateTime
21+
import java.util.UUID
1822
import javax.swing.*
1923
import javax.swing.border.CompoundBorder
2024
import javax.swing.border.EmptyBorder
@@ -37,6 +41,8 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
3741
border = JBUI.Borders.empty(8)
3842
}
3943

44+
private val messageLogPanel = McpMessageLogPanel()
45+
4046
private val responseScrollPane = JBScrollPane(rawResultTextArea).apply {
4147
border = BorderFactory.createEmptyBorder()
4248
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
@@ -50,11 +56,13 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
5056
private val tabbedPane = JBTabbedPane().apply {
5157
addTab(AutoDevBundle.message("mcp.chat.result.tab.response"), responseScrollPane)
5258
addTab(AutoDevBundle.message("mcp.chat.result.tab.tools"), toolsScrollPane)
59+
addTab(AutoDevBundle.message("mcp.chat.result.tab.messages"), messageLogPanel)
5360
}
5461

5562
private val borderColor = JBColor(0xE5E7EB, 0x3C3F41) // Equivalent to Tailwind gray-200
5663

5764
private var currentHeight = 300
65+
private var toolCalls: List<ToolCall> = emptyList()
5866

5967
init {
6068
background = UIUtil.getPanelBackground()
@@ -74,7 +82,7 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
7482
private fun parseAndShowTools(text: String) {
7583
toolsPanel.removeAll()
7684

77-
val toolCalls = ToolCall.fromString(text)
85+
toolCalls = ToolCall.fromString(text)
7886
if (toolCalls.isEmpty()) {
7987
val noToolsLabel = JBLabel(AutoDevBundle.message("mcp.chat.result.no.tools")).apply {
8088
foreground = JBColor(0x6B7280, 0x9DA0A8) // Gray text
@@ -92,6 +100,29 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
92100
} else {
93101
var gridY = 0
94102

103+
// Add "Execute All Tools" button
104+
val executeAllButton = JButton(AutoDevBundle.message("mcp.chat.result.execute.all")).apply {
105+
font = JBUI.Fonts.label(12f).asBold()
106+
addActionListener {
107+
executeAllTools()
108+
}
109+
}
110+
111+
val buttonPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
112+
isOpaque = false
113+
add(executeAllButton)
114+
}
115+
116+
val buttonGbc = GridBagConstraints().apply {
117+
gridx = 0
118+
gridy = gridY++
119+
weightx = 1.0
120+
fill = GridBagConstraints.HORIZONTAL
121+
insets = JBUI.insetsBottom(10)
122+
}
123+
124+
toolsPanel.add(buttonPanel, buttonGbc)
125+
95126
toolCalls.forEach { toolCall ->
96127
val toolPanel = createToolCallPanel(toolCall)
97128

@@ -128,6 +159,51 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
128159
}
129160
}
130161

162+
private fun executeAllTools() {
163+
if (toolCalls.isEmpty()) return
164+
165+
SwingUtilities.invokeLater {
166+
for (toolCall in toolCalls) {
167+
val matchingTool = findMatchingTool(toolCall.name)
168+
if (matchingTool != null) {
169+
val startTime = System.currentTimeMillis()
170+
171+
// Log request message
172+
val params = try {
173+
json.encodeToString(toolCall.parameters)
174+
} catch (e: Exception) {
175+
"{}"
176+
}
177+
178+
val requestMessage = McpMessage(
179+
type = MessageType.REQUEST,
180+
method = toolCall.name,
181+
timestamp = LocalDateTime.now(),
182+
content = "Tool: ${toolCall.name}\nParameters: $params"
183+
)
184+
messageLogPanel.addMessage(requestMessage)
185+
186+
// Execute the tool
187+
val result = mcpServerManager.execute(project, matchingTool, params)
188+
val duration = System.currentTimeMillis() - startTime
189+
190+
// Log response message
191+
val responseMessage = McpMessage(
192+
type = MessageType.RESPONSE,
193+
method = toolCall.name,
194+
timestamp = LocalDateTime.now(),
195+
duration = duration,
196+
content = result
197+
)
198+
messageLogPanel.addMessage(responseMessage)
199+
}
200+
}
201+
202+
// Switch to messages tab
203+
tabbedPane.selectedIndex = 2
204+
}
205+
}
206+
131207
private fun createToolCallPanel(toolCall: ToolCall): JPanel {
132208
val panel = JPanel(BorderLayout()).apply {
133209
background = UIUtil.getPanelBackground().brighter()
@@ -240,14 +316,33 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
240316
"{}"
241317
}
242318

319+
// Log request message
320+
val requestMessage = McpMessage(
321+
type = MessageType.REQUEST,
322+
method = toolCall.name,
323+
timestamp = LocalDateTime.now(),
324+
content = "Tool: ${toolCall.name}\nParameters: $params"
325+
)
326+
messageLogPanel.addMessage(requestMessage)
327+
243328
val matchingTool = findMatchingTool(toolCall.name)
244329
val result = if (matchingTool != null) {
245330
mcpServerManager.execute(project, matchingTool, params)
246331
} else {
247332
AutoDevBundle.message("mcp.chat.result.error.tool.not.found", toolCall.name)
248333
}
249334

335+
// Log response message
250336
val executionTime = System.currentTimeMillis() - startTime
337+
val responseMessage = McpMessage(
338+
type = MessageType.RESPONSE,
339+
method = toolCall.name,
340+
timestamp = LocalDateTime.now(),
341+
duration = executionTime,
342+
content = result
343+
)
344+
messageLogPanel.addMessage(responseMessage)
345+
251346
resultPanel.removeAll()
252347

253348
val timeInfoPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
@@ -290,4 +385,3 @@ class McpChatResultPanel(private val project: Project, val config: McpChatConfig
290385
return null
291386
}
292387
}
293-
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package cc.unitmesh.devti.mcp.ui
2+
3+
import cc.unitmesh.devti.mcp.ui.model.McpMessage
4+
import cc.unitmesh.devti.mcp.ui.model.MessageType
5+
import com.intellij.ui.JBColor
6+
import com.intellij.ui.components.JBScrollPane
7+
import com.intellij.ui.table.JBTable
8+
import com.intellij.util.ui.JBUI
9+
import java.awt.BorderLayout
10+
import java.awt.Component
11+
import java.awt.Dimension
12+
import java.time.format.DateTimeFormatter
13+
import javax.swing.*
14+
import javax.swing.table.DefaultTableCellRenderer
15+
import javax.swing.table.DefaultTableModel
16+
17+
class McpMessageLogPanel : JPanel(BorderLayout()) {
18+
private val messages = mutableListOf<McpMessage>()
19+
private val tableModel = MessageTableModel()
20+
private val table = JBTable(tableModel).apply {
21+
setShowGrid(false)
22+
intercellSpacing = Dimension(0, 0)
23+
rowHeight = 30
24+
selectionModel = DefaultListSelectionModel().apply {
25+
selectionMode = ListSelectionModel.SINGLE_SELECTION
26+
}
27+
autoCreateRowSorter = true
28+
}
29+
30+
private val detailTextArea = JTextArea().apply {
31+
isEditable = false
32+
wrapStyleWord = true
33+
lineWrap = true
34+
border = JBUI.Borders.empty(10)
35+
}
36+
37+
init {
38+
table.getColumnModel().getColumn(0).cellRenderer = TypeColumnRenderer()
39+
40+
// Create split pane
41+
val splitPane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT).apply {
42+
leftComponent = JBScrollPane(table)
43+
rightComponent = JBScrollPane(detailTextArea)
44+
dividerLocation = 600
45+
resizeWeight = 0.5
46+
}
47+
48+
add(splitPane, BorderLayout.CENTER)
49+
table.selectionModel.addListSelectionListener { e ->
50+
if (!e.valueIsAdjusting && table.selectedRow >= 0) {
51+
val selectedIndex = table.convertRowIndexToModel(table.selectedRow)
52+
if (selectedIndex >= 0 && selectedIndex < messages.size) {
53+
detailTextArea.text = messages[selectedIndex].content
54+
detailTextArea.caretPosition = 0
55+
}
56+
}
57+
}
58+
}
59+
60+
fun addMessage(message: McpMessage) {
61+
messages.add(message)
62+
tableModel.fireTableDataChanged()
63+
SwingUtilities.invokeLater {
64+
table.setRowSelectionInterval(messages.size - 1, messages.size - 1)
65+
}
66+
}
67+
68+
fun clear() {
69+
messages.clear()
70+
tableModel.fireTableDataChanged()
71+
detailTextArea.text = ""
72+
}
73+
74+
private inner class MessageTableModel : DefaultTableModel() {
75+
private val columnNames = arrayOf(
76+
"Type",
77+
"Method",
78+
"Timestamp",
79+
"Duration"
80+
)
81+
82+
override fun getColumnCount(): Int = columnNames.size
83+
84+
override fun getRowCount(): Int = messages.size
85+
86+
override fun getColumnName(column: Int): String = columnNames[column]
87+
88+
override fun getValueAt(row: Int, column: Int): Any {
89+
val message = messages[row]
90+
return when (column) {
91+
0 -> message.type
92+
1 -> message.method
93+
2 -> message.timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
94+
3 -> message.duration?.toString() ?: "-"
95+
else -> ""
96+
}
97+
}
98+
99+
override fun isCellEditable(row: Int, column: Int): Boolean = false
100+
}
101+
102+
private class TypeColumnRenderer : DefaultTableCellRenderer() {
103+
override fun getTableCellRendererComponent(
104+
table: JTable?,
105+
value: Any?,
106+
isSelected: Boolean,
107+
hasFocus: Boolean,
108+
row: Int,
109+
column: Int
110+
): Component {
111+
val label = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) as JLabel
112+
label.horizontalAlignment = SwingConstants.CENTER
113+
114+
if (!isSelected) {
115+
when (value) {
116+
MessageType.REQUEST -> {
117+
label.background = JBColor(0xE1F5FE, 0x0D47A1) // Light blue for request
118+
label.foreground = JBColor(0x01579B, 0xE3F2FD)
119+
}
120+
MessageType.RESPONSE -> {
121+
label.background = JBColor(0xE8F5E9, 0x2E7D32) // Light green for response
122+
label.foreground = JBColor(0x1B5E20, 0xE8F5E9)
123+
}
124+
}
125+
}
126+
127+
label.text = when (value) {
128+
MessageType.REQUEST -> "REQUEST"
129+
MessageType.RESPONSE -> "RESPONSE"
130+
else -> ""
131+
}
132+
133+
label.border = JBUI.Borders.empty(3, 5)
134+
135+
return label
136+
}
137+
}
138+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cc.unitmesh.devti.mcp.ui.model
2+
3+
import java.time.LocalDateTime
4+
5+
enum class MessageType {
6+
REQUEST,
7+
RESPONSE
8+
}
9+
10+
data class McpMessage(
11+
val type: MessageType,
12+
val method: String,
13+
val timestamp: LocalDateTime,
14+
val duration: Long? = null,
15+
val content: String
16+
)

core/src/main/resources/messages/AutoDevBundle_en.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ mcp.chat.result.no.tools=No tool calls found in the response
241241
mcp.chat.result.execute=Execute
242242
mcp.chat.result.executing=Executing tool {0}...
243243
mcp.chat.result.error.tool.not.found=Error: Could not find matching tool ''{0}''
244-
245-
indexer.generate.domain=Generate domain.csv
246244
mcp.chat.result.execution.time=Duration
245+
mcp.chat.result.execute.all=Execute All Tools
246+
mcp.chat.result.tab.messages=Message Log
247247

248+
indexer.generate.domain=Generate domain.csv

core/src/main/resources/messages/AutoDevBundle_zh.properties

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,6 @@ chat.panel.add.openFiles=添加所有打开的文件
224224
mcp.chat.config.dialog.title=模型配置
225225
mcp.chat.config.dialog.temperature=温度: {0}
226226
mcp.chat.config.dialog.enabled.tools=已启用工具
227-
228-
indexer.generate.domain=生成 domain.csv
229-
230227
# MCP Chat Result Panel
231228
mcp.chat.result.tab.response=响应
232229
mcp.chat.result.tab.tools=工具
@@ -235,4 +232,6 @@ mcp.chat.result.execute=执行
235232
mcp.chat.result.executing=正在执行工具 {0}...
236233
mcp.chat.result.error.tool.not.found=错误:找不到匹配的工具 ''{0}''
237234
mcp.chat.result.execution.time=Duration
238-
235+
mcp.chat.result.execute.all=Execute All Tools
236+
mcp.chat.result.tab.messages=Message Log
237+
indexer.generate.domain=生成 domain.csv

0 commit comments

Comments
 (0)