diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index 9e48552fdc..c1b0dc6871 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -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(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 diff --git a/webview-ui/src/components/mcp/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx index 5dea579193..1745867ba9 100644 --- a/webview-ui/src/components/mcp/McpToolRow.tsx +++ b/webview-ui/src/components/mcp/McpToolRow.tsx @@ -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 {tool.description && (
- {tool.description} + className={cn("mt-1 text-xs text-vscode-descriptionForeground", { + "opacity-80": isToolEnabled, + "opacity-40": !isToolEnabled, + })}> + {isInChatContext ? ( +
+ + + + + setIsDescriptionExpanded(!isDescriptionExpanded)}> + {tool.description} + + +
+ ) : ( + tool.description + )}
)} {isToolEnabled && diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx index f686a23f00..e7218932ac 100644 --- a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx +++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx @@ -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() + + // 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() + + // 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() + + 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() + + 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() + + 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") + }) + }) }) diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 2fcae2840c..b5e01cec33 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -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",