Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit d9c4bb4

Browse files
committed
feat: reduce MCP tool call UI clutter with collapsible descriptions and path truncation
- Add collapsible description in McpToolRow when in chat context - Truncate workspace root paths in MCP arguments for cleaner display - Add expand/collapse toggle button with chevron icon - Add tooltip with full description on hover - Add new translation keys for expand/collapse actions - Add tests for collapsible description functionality Addresses #11155
1 parent b020f6b commit d9c4bb4

4 files changed

Lines changed: 144 additions & 11 deletions

File tree

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,35 @@ import { safeJsonParse } from "@roo/core"
1414

1515
import { cn } from "@src/lib/utils"
1616
import { Button } from "@src/components/ui"
17+
import { useExtensionState } from "@src/context/ExtensionStateContext"
1718

1819
import CodeBlock from "../common/CodeBlock"
1920
import McpToolRow from "../mcp/McpToolRow"
2021

2122
import { Markdown } from "./Markdown"
2223

24+
/**
25+
* Truncates workspace paths in a JSON string by replacing the workspace root with "./"
26+
* @param jsonString - The JSON string containing paths to truncate
27+
* @param cwd - The current workspace directory (optional)
28+
* @returns The JSON string with truncated paths
29+
*/
30+
function truncateWorkspacePaths(jsonString: string, cwd?: string): string {
31+
if (!jsonString || !cwd) return jsonString
32+
33+
// Normalize the cwd to handle different path separators
34+
const normalizedCwd = cwd.replace(/\\/g, "/")
35+
36+
// Escape special regex characters in the path
37+
const escapedCwd = normalizedCwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
38+
39+
// Create a regex that matches the workspace path (with optional trailing slash)
40+
const workspacePathRegex = new RegExp(escapedCwd + "/?", "g")
41+
42+
// Replace workspace paths with "./"
43+
return jsonString.replace(workspacePathRegex, "./")
44+
}
45+
2346
interface McpExecutionProps {
2447
executionId: string
2548
text?: string
@@ -49,6 +72,7 @@ export const McpExecution = ({
4972
alwaysAllowMcp = false,
5073
}: McpExecutionProps) => {
5174
const { t } = useTranslation("mcp")
75+
const { cwd } = useExtensionState()
5276

5377
// State for tracking MCP response status
5478
const [status, setStatus] = useState<McpExecutionStatus | null>(null)
@@ -92,6 +116,7 @@ export const McpExecution = ({
92116
}, [responseText, isResponseExpanded, tryParseJson, status])
93117

94118
// Only parse arguments data when complete to avoid parsing partial JSON
119+
// Also apply workspace path truncation for cleaner display
95120
const argumentsData = useMemo(() => {
96121
if (!argumentsText) {
97122
return { isJson: false, formatted: "" }
@@ -108,19 +133,22 @@ export const McpExecution = ({
108133
// Try to parse, but if it fails, return as-is
109134
try {
110135
const parsed = JSON.parse(trimmed)
136+
const formatted = JSON.stringify(parsed, null, 2)
137+
// Apply path truncation to show cleaner paths
138+
const truncated = truncateWorkspacePaths(formatted, cwd)
111139
return {
112140
isJson: true,
113-
formatted: JSON.stringify(parsed, null, 2),
141+
formatted: truncated,
114142
}
115143
} catch {
116-
// JSON structure looks complete but is invalid, return as-is
117-
return { isJson: false, formatted: argumentsText }
144+
// JSON structure looks complete but is invalid, return as-is (but still truncate paths)
145+
return { isJson: false, formatted: truncateWorkspacePaths(argumentsText, cwd) }
118146
}
119147
}
120148

121-
// For non-JSON or incomplete data, just return as-is
122-
return { isJson: false, formatted: argumentsText }
123-
}, [argumentsText])
149+
// For non-JSON or incomplete data, just return as-is (but still truncate paths)
150+
return { isJson: false, formatted: truncateWorkspacePaths(argumentsText, cwd) }
151+
}, [argumentsText, cwd])
124152

125153
const formattedResponseText = responseData.formatted
126154
const formattedArgumentsText = argumentsData.formatted

webview-ui/src/components/mcp/McpToolRow.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { useState } from "react"
12
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
3+
import { ChevronDown } from "lucide-react"
24

35
import type { McpTool } from "@roo-code/types"
46

57
import { useAppTranslation } from "@src/i18n/TranslationContext"
68
import { vscode } from "@src/utils/vscode"
9+
import { cn } from "@src/lib/utils"
710
import { StandardTooltip, ToggleSwitch } from "@/components/ui"
811

912
type McpToolRowProps = {
@@ -17,6 +20,7 @@ type McpToolRowProps = {
1720
const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => {
1821
const { t } = useAppTranslation()
1922
const isToolEnabled = tool.enabledForPrompt ?? true
23+
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
2024

2125
const handleAlwaysAllowChange = () => {
2226
if (!serverName) return
@@ -99,10 +103,39 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
99103
</div>
100104
{tool.description && (
101105
<div
102-
className={`mt-1 text-xs text-vscode-descriptionForeground ${
103-
isToolEnabled ? "opacity-80" : "opacity-40"
104-
}`}>
105-
{tool.description}
106+
className={cn("mt-1 text-xs text-vscode-descriptionForeground", {
107+
"opacity-80": isToolEnabled,
108+
"opacity-40": !isToolEnabled,
109+
})}>
110+
{isInChatContext ? (
111+
<div className="flex items-start gap-1">
112+
<StandardTooltip
113+
content={isDescriptionExpanded ? t("mcp:tool.collapse") : t("mcp:tool.expand")}>
114+
<button
115+
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}
116+
className="flex items-center p-0.5 -ml-0.5 rounded hover:bg-vscode-list-hoverBackground"
117+
data-testid="description-toggle">
118+
<ChevronDown
119+
className={cn("size-3 transition-transform duration-200", {
120+
"rotate-0": isDescriptionExpanded,
121+
"-rotate-90": !isDescriptionExpanded,
122+
})}
123+
/>
124+
</button>
125+
</StandardTooltip>
126+
<StandardTooltip content={tool.description}>
127+
<span
128+
className={cn("cursor-pointer", {
129+
"line-clamp-2": !isDescriptionExpanded,
130+
})}
131+
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}>
132+
{tool.description}
133+
</span>
134+
</StandardTooltip>
135+
</div>
136+
) : (
137+
tool.description
138+
)}
106139
</div>
107140
)}
108141
{isToolEnabled &&

webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ vi.mock("@src/i18n/TranslationContext", () => ({
1313
"mcp:tool.parameters": "Parameters",
1414
"mcp:tool.noDescription": "No description",
1515
"mcp:tool.togglePromptInclusion": "Toggle prompt inclusion",
16+
"mcp:tool.expand": "Expand description",
17+
"mcp:tool.collapse": "Collapse description",
1618
}
1719
return translations[key] || key
1820
},
@@ -285,4 +287,72 @@ describe("McpToolRow", () => {
285287
expect(toolDescription).toHaveClass("opacity-80")
286288
expect(toolDescription).not.toHaveClass("opacity-40")
287289
})
290+
291+
describe("collapsible description in chat context", () => {
292+
const toolWithLongDescription = {
293+
...mockTool,
294+
description:
295+
"This is a very long description that should be truncated to two lines when displayed in chat context. It provides detailed information about what the tool does and how it works.",
296+
}
297+
298+
it("shows collapsible description when isInChatContext is true", () => {
299+
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
300+
301+
// Should have the toggle button
302+
const toggleButton = screen.getByTestId("description-toggle")
303+
expect(toggleButton).toBeInTheDocument()
304+
})
305+
306+
it("does not show collapsible description when isInChatContext is false", () => {
307+
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={false} />)
308+
309+
// Should not have the toggle button
310+
const toggleButton = screen.queryByTestId("description-toggle")
311+
expect(toggleButton).not.toBeInTheDocument()
312+
})
313+
314+
it("expands description when toggle button is clicked", () => {
315+
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
316+
317+
const toggleButton = screen.getByTestId("description-toggle")
318+
319+
// Initially collapsed - description should have line-clamp-2 class
320+
const descriptionText = screen.getByText(toolWithLongDescription.description)
321+
expect(descriptionText).toHaveClass("line-clamp-2")
322+
323+
// Click to expand
324+
fireEvent.click(toggleButton)
325+
326+
// After expanding - description should not have line-clamp-2 class
327+
expect(descriptionText).not.toHaveClass("line-clamp-2")
328+
})
329+
330+
it("collapses description when toggle button is clicked twice", () => {
331+
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
332+
333+
const toggleButton = screen.getByTestId("description-toggle")
334+
const descriptionText = screen.getByText(toolWithLongDescription.description)
335+
336+
// Click to expand
337+
fireEvent.click(toggleButton)
338+
expect(descriptionText).not.toHaveClass("line-clamp-2")
339+
340+
// Click again to collapse
341+
fireEvent.click(toggleButton)
342+
expect(descriptionText).toHaveClass("line-clamp-2")
343+
})
344+
345+
it("expands description when clicking on the description text", () => {
346+
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
347+
348+
const descriptionText = screen.getByText(toolWithLongDescription.description)
349+
expect(descriptionText).toHaveClass("line-clamp-2")
350+
351+
// Click on the description text
352+
fireEvent.click(descriptionText)
353+
354+
// Should expand
355+
expect(descriptionText).not.toHaveClass("line-clamp-2")
356+
})
357+
})
288358
})

webview-ui/src/i18n/locales/en/mcp.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"alwaysAllow": "Always allow",
2222
"parameters": "Parameters",
2323
"noDescription": "No description",
24-
"togglePromptInclusion": "Toggle inclusion in prompt"
24+
"togglePromptInclusion": "Toggle inclusion in prompt",
25+
"expand": "Expand description",
26+
"collapse": "Collapse description"
2527
},
2628
"tabs": {
2729
"tools": "Tools",

0 commit comments

Comments
 (0)