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
This commit is contained in:
Roo Code 2026-02-02 18:02:03 +00:00
parent b020f6be43
commit d9c4bb45b6
4 changed files with 144 additions and 11 deletions

View file

@ -14,12 +14,35 @@ import { safeJsonParse } from "@roo/core"
import { cn } from "@src/lib/utils"
import { Button } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import CodeBlock from "../common/CodeBlock"
import McpToolRow from "../mcp/McpToolRow"
import { Markdown } from "./Markdown"
/**
* Truncates workspace paths in a JSON string by replacing the workspace root with "./"
* @param jsonString - The JSON string containing paths to truncate
* @param cwd - The current workspace directory (optional)
* @returns The JSON string with truncated paths
*/
function truncateWorkspacePaths(jsonString: string, cwd?: string): string {
if (!jsonString || !cwd) return jsonString
// Normalize the cwd to handle different path separators
const normalizedCwd = cwd.replace(/\\/g, "/")
// Escape special regex characters in the path
const escapedCwd = normalizedCwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
// Create a regex that matches the workspace path (with optional trailing slash)
const workspacePathRegex = new RegExp(escapedCwd + "/?", "g")
// Replace workspace paths with "./"
return jsonString.replace(workspacePathRegex, "./")
}
interface McpExecutionProps {
executionId: string
text?: string
@ -49,6 +72,7 @@ export const McpExecution = ({
alwaysAllowMcp = false,
}: McpExecutionProps) => {
const { t } = useTranslation("mcp")
const { cwd } = useExtensionState()
// State for tracking MCP response status
const [status, setStatus] = useState<McpExecutionStatus | null>(null)
@ -92,6 +116,7 @@ export const McpExecution = ({
}, [responseText, isResponseExpanded, tryParseJson, status])
// Only parse arguments data when complete to avoid parsing partial JSON
// Also apply workspace path truncation for cleaner display
const argumentsData = useMemo(() => {
if (!argumentsText) {
return { isJson: false, formatted: "" }
@ -108,19 +133,22 @@ export const McpExecution = ({
// Try to parse, but if it fails, return as-is
try {
const parsed = JSON.parse(trimmed)
const formatted = JSON.stringify(parsed, null, 2)
// Apply path truncation to show cleaner paths
const truncated = truncateWorkspacePaths(formatted, cwd)
return {
isJson: true,
formatted: JSON.stringify(parsed, null, 2),
formatted: truncated,
}
} catch {
// JSON structure looks complete but is invalid, return as-is
return { isJson: false, formatted: argumentsText }
// JSON structure looks complete but is invalid, return as-is (but still truncate paths)
return { isJson: false, formatted: truncateWorkspacePaths(argumentsText, cwd) }
}
}
// For non-JSON or incomplete data, just return as-is
return { isJson: false, formatted: argumentsText }
}, [argumentsText])
// For non-JSON or incomplete data, just return as-is (but still truncate paths)
return { isJson: false, formatted: truncateWorkspacePaths(argumentsText, cwd) }
}, [argumentsText, cwd])
const formattedResponseText = responseData.formatted
const formattedArgumentsText = argumentsData.formatted

View file

@ -1,9 +1,12 @@
import { useState } from "react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { ChevronDown } from "lucide-react"
import type { McpTool } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { cn } from "@src/lib/utils"
import { StandardTooltip, ToggleSwitch } from "@/components/ui"
type McpToolRowProps = {
@ -17,6 +20,7 @@ type McpToolRowProps = {
const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => {
const { t } = useAppTranslation()
const isToolEnabled = tool.enabledForPrompt ?? true
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const handleAlwaysAllowChange = () => {
if (!serverName) return
@ -99,10 +103,39 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
</div>
{tool.description && (
<div
className={`mt-1 text-xs text-vscode-descriptionForeground ${
isToolEnabled ? "opacity-80" : "opacity-40"
}`}>
{tool.description}
className={cn("mt-1 text-xs text-vscode-descriptionForeground", {
"opacity-80": isToolEnabled,
"opacity-40": !isToolEnabled,
})}>
{isInChatContext ? (
<div className="flex items-start gap-1">
<StandardTooltip
content={isDescriptionExpanded ? t("mcp:tool.collapse") : t("mcp:tool.expand")}>
<button
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}
className="flex items-center p-0.5 -ml-0.5 rounded hover:bg-vscode-list-hoverBackground"
data-testid="description-toggle">
<ChevronDown
className={cn("size-3 transition-transform duration-200", {
"rotate-0": isDescriptionExpanded,
"-rotate-90": !isDescriptionExpanded,
})}
/>
</button>
</StandardTooltip>
<StandardTooltip content={tool.description}>
<span
className={cn("cursor-pointer", {
"line-clamp-2": !isDescriptionExpanded,
})}
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}>
{tool.description}
</span>
</StandardTooltip>
</div>
) : (
tool.description
)}
</div>
)}
{isToolEnabled &&

View file

@ -13,6 +13,8 @@ vi.mock("@src/i18n/TranslationContext", () => ({
"mcp:tool.parameters": "Parameters",
"mcp:tool.noDescription": "No description",
"mcp:tool.togglePromptInclusion": "Toggle prompt inclusion",
"mcp:tool.expand": "Expand description",
"mcp:tool.collapse": "Collapse description",
}
return translations[key] || key
},
@ -285,4 +287,72 @@ describe("McpToolRow", () => {
expect(toolDescription).toHaveClass("opacity-80")
expect(toolDescription).not.toHaveClass("opacity-40")
})
describe("collapsible description in chat context", () => {
const toolWithLongDescription = {
...mockTool,
description:
"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.",
}
it("shows collapsible description when isInChatContext is true", () => {
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
// Should have the toggle button
const toggleButton = screen.getByTestId("description-toggle")
expect(toggleButton).toBeInTheDocument()
})
it("does not show collapsible description when isInChatContext is false", () => {
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={false} />)
// Should not have the toggle button
const toggleButton = screen.queryByTestId("description-toggle")
expect(toggleButton).not.toBeInTheDocument()
})
it("expands description when toggle button is clicked", () => {
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
const toggleButton = screen.getByTestId("description-toggle")
// Initially collapsed - description should have line-clamp-2 class
const descriptionText = screen.getByText(toolWithLongDescription.description)
expect(descriptionText).toHaveClass("line-clamp-2")
// Click to expand
fireEvent.click(toggleButton)
// After expanding - description should not have line-clamp-2 class
expect(descriptionText).not.toHaveClass("line-clamp-2")
})
it("collapses description when toggle button is clicked twice", () => {
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
const toggleButton = screen.getByTestId("description-toggle")
const descriptionText = screen.getByText(toolWithLongDescription.description)
// Click to expand
fireEvent.click(toggleButton)
expect(descriptionText).not.toHaveClass("line-clamp-2")
// Click again to collapse
fireEvent.click(toggleButton)
expect(descriptionText).toHaveClass("line-clamp-2")
})
it("expands description when clicking on the description text", () => {
render(<McpToolRow tool={toolWithLongDescription} isInChatContext={true} />)
const descriptionText = screen.getByText(toolWithLongDescription.description)
expect(descriptionText).toHaveClass("line-clamp-2")
// Click on the description text
fireEvent.click(descriptionText)
// Should expand
expect(descriptionText).not.toHaveClass("line-clamp-2")
})
})
})

View file

@ -21,7 +21,9 @@
"alwaysAllow": "Always allow",
"parameters": "Parameters",
"noDescription": "No description",
"togglePromptInclusion": "Toggle inclusion in prompt"
"togglePromptInclusion": "Toggle inclusion in prompt",
"expand": "Expand description",
"collapse": "Collapse description"
},
"tabs": {
"tools": "Tools",