Skip to content

Commit 0221aaa

Browse files
mcowgerChris Hasson
andauthored
feat(chat): Add collapsible MCP tool calls with memory management (#1886)
- Implement single-level collapsible functionality for MCP tool calls in chat - Remove duplicate response-level collapse chevron, keeping only main toggle - Set default state to collapsed for cleaner chat interface - Preserve memory management by using main collapse state for response parsing - Maintain performance optimizations for large responses when collapsed Co-authored-by: Chris Hasson <[email protected]>
1 parent ea3a743 commit 0221aaa

File tree

3 files changed

+199
-16
lines changed

3 files changed

+199
-16
lines changed

.changeset/fluffy-parents-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": minor
3+
---
4+
5+
Add collapsible MCP tool calls with memory management
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite"
2+
import { fn } from "storybook/test"
3+
4+
import { McpExecution } from "../../../webview-ui/src/components/chat/McpExecution"
5+
import { withTooltipProvider } from "../src/decorators/withTooltipProvider"
6+
7+
const meta = {
8+
title: "Chat/Row/McpExecution",
9+
component: McpExecution,
10+
parameters: { layout: "padded" },
11+
} satisfies Meta<typeof McpExecution>
12+
13+
export default meta
14+
type Story = StoryObj<typeof meta>
15+
16+
// Mock server configuration
17+
const mockServer = {
18+
tools: [
19+
{
20+
name: "get_weather",
21+
description: "Get current weather information for a location",
22+
alwaysAllow: false,
23+
},
24+
{
25+
name: "search_files",
26+
description: "Search for files in the project directory",
27+
alwaysAllow: true,
28+
},
29+
],
30+
source: "global" as const,
31+
}
32+
33+
// Sample JSON arguments
34+
const jsonArguments = JSON.stringify(
35+
{
36+
location: "San Francisco, CA",
37+
units: "metric",
38+
include_forecast: true,
39+
},
40+
null,
41+
2,
42+
)
43+
44+
// Sample JSON response
45+
const jsonResponse = JSON.stringify(
46+
{
47+
location: "San Francisco, CA",
48+
temperature: 18,
49+
humidity: 65,
50+
conditions: "Partly cloudy",
51+
forecast: [
52+
{ day: "Tomorrow", high: 20, low: 15, conditions: "Sunny" },
53+
{ day: "Tuesday", high: 19, low: 14, conditions: "Cloudy" },
54+
],
55+
},
56+
null,
57+
2,
58+
)
59+
60+
export const CompletedWithJsonResponse: Story = {
61+
name: "Completed - JSON Response",
62+
args: {
63+
executionId: "exec-003",
64+
text: jsonArguments,
65+
serverName: "weather-server",
66+
useMcpServer: {
67+
type: "use_mcp_tool",
68+
serverName: "weather-server",
69+
toolName: "get_weather",
70+
arguments: jsonArguments,
71+
response: jsonResponse,
72+
},
73+
server: mockServer,
74+
alwaysAllowMcp: false,
75+
initiallyExpanded: true,
76+
},
77+
}
78+
79+
export const CompletedWithMarkdownResponse: Story = {
80+
name: "Completed - Markdown Response",
81+
args: {
82+
executionId: "exec-004",
83+
text: jsonArguments,
84+
serverName: "weather-server",
85+
useMcpServer: {
86+
type: "use_mcp_tool",
87+
serverName: "weather-server",
88+
toolName: "get_weather",
89+
arguments: jsonArguments,
90+
response: `# Weather Report
91+
92+
**Location:** San Francisco, CA
93+
**Temperature:** 18°C
94+
**Humidity:** 65%
95+
**Conditions:** Partly cloudy
96+
97+
## 3-Day Forecast
98+
99+
- **Tomorrow:** Sunny, High: 20°C, Low: 15°C
100+
- **Tuesday:** Cloudy, High: 19°C, Low: 14°C
101+
- **Wednesday:** Rain, High: 17°C, Low: 12°C`,
102+
},
103+
server: mockServer,
104+
alwaysAllowMcp: false,
105+
initiallyExpanded: true,
106+
},
107+
}
108+
109+
export const RunningState: Story = {
110+
name: "Running State",
111+
args: {
112+
executionId: "exec-005",
113+
text: jsonArguments,
114+
serverName: "weather-server",
115+
useMcpServer: {
116+
type: "use_mcp_tool",
117+
serverName: "weather-server",
118+
toolName: "get_weather",
119+
arguments: jsonArguments,
120+
},
121+
server: mockServer,
122+
alwaysAllowMcp: false,
123+
},
124+
play: async ({ canvasElement }) => {
125+
// Simulate a running execution by dispatching a status message
126+
window.dispatchEvent(
127+
new MessageEvent("message", {
128+
data: {
129+
type: "mcpExecutionStatus",
130+
text: JSON.stringify({
131+
executionId: "exec-005",
132+
status: "started",
133+
serverName: "weather-server",
134+
toolName: "get_weather",
135+
}),
136+
},
137+
}),
138+
)
139+
},
140+
}
141+
142+
export const ErrorState: Story = {
143+
name: "Error State",
144+
args: {
145+
executionId: "exec-006",
146+
text: jsonArguments,
147+
serverName: "weather-server",
148+
useMcpServer: {
149+
type: "use_mcp_tool",
150+
serverName: "weather-server",
151+
toolName: "get_weather",
152+
arguments: jsonArguments,
153+
},
154+
server: mockServer,
155+
alwaysAllowMcp: false,
156+
},
157+
play: async ({ canvasElement }) => {
158+
// Simulate an error state
159+
window.dispatchEvent(
160+
new MessageEvent("message", {
161+
data: {
162+
type: "mcpExecutionStatus",
163+
text: JSON.stringify({
164+
executionId: "exec-006",
165+
status: "error",
166+
error: "Connection timeout to weather service",
167+
}),
168+
},
169+
}),
170+
)
171+
},
172+
}

webview-ui/src/components/chat/McpExecution.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useMemo, useState, memo } from "react"
2-
import { Server, ChevronDown } from "lucide-react"
2+
import { Server, ChevronDown, ChevronRight } from "lucide-react"
33
import { useEvent } from "react-use"
44
import { useTranslation } from "react-i18next"
55

@@ -28,6 +28,7 @@ interface McpExecutionProps {
2828
}
2929
useMcpServer?: ClineAskUseMcpServer
3030
alwaysAllowMcp?: boolean
31+
initiallyExpanded?: boolean // kilocode_change: For Storybook stories only
3132
}
3233

3334
export const McpExecution = ({
@@ -39,6 +40,7 @@ export const McpExecution = ({
3940
server,
4041
useMcpServer,
4142
alwaysAllowMcp = false,
43+
initiallyExpanded = false, // kilocode_change
4244
}: McpExecutionProps) => {
4345
const { t } = useTranslation("mcp")
4446

@@ -49,8 +51,8 @@ export const McpExecution = ({
4951
const [serverName, setServerName] = useState(initialServerName)
5052
const [toolName, setToolName] = useState(initialToolName)
5153

52-
// Only need expanded state for response section (like command output)
53-
const [isResponseExpanded, setIsResponseExpanded] = useState(false)
54+
// kilocode_change: Main collapse state for the entire MCP execution content
55+
const [isResponseExpanded, setIsResponseExpanded] = useState(initiallyExpanded)
5456

5557
// Try to parse JSON and return both the result and formatted text
5658
const tryParseJson = useCallback((text: string): { isJson: boolean; formatted: string } => {
@@ -70,7 +72,7 @@ export const McpExecution = ({
7072
}
7173
}, [])
7274

73-
// Only parse response data when expanded AND complete to avoid parsing partial JSON
75+
// kilocode_change: Only parse response data when main content is expanded AND complete to avoid parsing partial JSON
7476
const responseData = useMemo(() => {
7577
if (!isResponseExpanded) {
7678
return { isJson: false, formatted: responseText }
@@ -178,7 +180,9 @@ export const McpExecution = ({
178180

179181
return (
180182
<>
181-
<div className="flex flex-row items-center justify-between gap-2 mb-1">
183+
<div
184+
className="flex flex-row items-center justify-between gap-2 mb-1 cursor-pointer select-none"
185+
onClick={onToggleResponseExpand /* kilocode_change */}>
182186
<div className="flex flex-row items-center gap-1 flex-wrap">
183187
<Server size={16} className="text-vscode-descriptionForeground" />
184188
<div className="flex items-center gap-1 flex-wrap">
@@ -212,20 +216,22 @@ export const McpExecution = ({
212216
)}
213217
</div>
214218
)}
215-
{responseText && responseText.length > 0 && (
216-
<Button variant="ghost" size="icon" onClick={onToggleResponseExpand}>
217-
<ChevronDown
218-
className={cn("size-4 transition-transform duration-300", {
219-
"rotate-180": isResponseExpanded,
220-
})}
221-
/>
222-
</Button>
223-
)}
224219
</div>
220+
{/* kilocode_change start - moved Chevron button */}
221+
<Button
222+
variant="ghost"
223+
size="icon"
224+
onClick={(e) => {
225+
e.stopPropagation()
226+
onToggleResponseExpand()
227+
}}>
228+
{!isResponseExpanded ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
229+
</Button>
230+
{/* kilocode_change end - moved Chevron button */}
225231
</div>
226232
</div>
227233

228-
<div className="w-full bg-vscode-editor-background rounded-xs p-2">
234+
<div className={cn("w-full bg-vscode-editor-background rounded-xs p-2", !isResponseExpanded && "hidden")}>
229235
{/* Tool information section */}
230236
{useMcpServer?.type === "use_mcp_tool" && (
231237
<div onClick={(e) => e.stopPropagation()}>
@@ -273,7 +279,7 @@ export const McpExecution = ({
273279
</div>
274280
)}
275281

276-
{/* Response section - collapsible like command output */}
282+
{/* Response section - use main collapse state for memory management */}
277283
<ResponseContainer
278284
isExpanded={isResponseExpanded}
279285
response={formattedResponseText}

0 commit comments

Comments
 (0)