Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions webview-ui/src/components/chat/McpExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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: "" }
Expand All @@ -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
Expand Down
41 changes: 37 additions & 4 deletions webview-ui/src/components/mcp/McpToolRow.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down
70 changes: 70 additions & 0 deletions webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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")
})
})
})
4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/ca/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/de/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/en/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/es/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/fr/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/hi/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/id/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/it/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/ja/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/ko/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/nl/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/pl/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/pt-BR/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/ru/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/src/i18n/locales/tr/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading