From f11eceb5c579c6467f38c39482446b16f182e6e9 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 5 Jun 2025 20:22:58 +0700 Subject: [PATCH 1/8] feat: add MCP viewer and enhanced UI - Add McpExecutionStatus type with discriminated union for execution states - Implement McpExecution component with live status updates and collapsible output - Replace inline MCP UI in ChatRow with dedicated McpExecution component - Add comprehensive test coverage for useMcpToolTool - Enhance CodeAccordian to support custom headers - Improve combineCommandSequences to handle MCP execution flows --- packages/types/src/index.ts | 1 + packages/types/src/mcp.ts | 31 ++ .../tools/__tests__/useMcpToolTool.test.ts | 260 ++++++++++++++++ src/core/tools/useMcpToolTool.ts | 272 ++++++++++++----- src/shared/ExtensionMessage.ts | 2 + .../__tests__/combineCommandSequences.test.ts | 218 +++++++++++--- src/shared/combineCommandSequences.ts | 81 ++++- webview-ui/src/components/chat/ChatRow.tsx | 94 ++---- .../src/components/chat/McpExecution.tsx | 280 ++++++++++++++++++ .../src/components/common/CodeAccordian.tsx | 17 +- 10 files changed, 1047 insertions(+), 209 deletions(-) create mode 100644 packages/types/src/mcp.ts create mode 100644 src/core/tools/__tests__/useMcpToolTool.test.ts create mode 100644 webview-ui/src/components/chat/McpExecution.tsx diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ac3926ac37..f9e546f095 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -7,6 +7,7 @@ export * from "./experiment.js" export * from "./global-settings.js" export * from "./history.js" export * from "./ipc.js" +export * from "./mcp.js" export * from "./message.js" export * from "./mode.js" export * from "./model.js" diff --git a/packages/types/src/mcp.ts b/packages/types/src/mcp.ts new file mode 100644 index 0000000000..e72d0633f7 --- /dev/null +++ b/packages/types/src/mcp.ts @@ -0,0 +1,31 @@ +import { z } from "zod" + +/** + * McpExecutionStatus + */ + +export const mcpExecutionStatusSchema = z.discriminatedUnion("status", [ + z.object({ + executionId: z.string(), + status: z.literal("started"), + serverName: z.string(), + toolName: z.string(), + }), + z.object({ + executionId: z.string(), + status: z.literal("output"), + response: z.string(), + }), + z.object({ + executionId: z.string(), + status: z.literal("completed"), + response: z.string().optional(), + }), + z.object({ + executionId: z.string(), + status: z.literal("error"), + error: z.string().optional(), + }), +]) + +export type McpExecutionStatus = z.infer diff --git a/src/core/tools/__tests__/useMcpToolTool.test.ts b/src/core/tools/__tests__/useMcpToolTool.test.ts new file mode 100644 index 0000000000..4c4ae5fbef --- /dev/null +++ b/src/core/tools/__tests__/useMcpToolTool.test.ts @@ -0,0 +1,260 @@ +import { useMcpToolTool } from "../useMcpToolTool" +import { Task } from "../../task/Task" +import { ToolUse } from "../../../shared/tools" +import { formatResponse } from "../../prompts/responses" + +// Mock dependencies +jest.mock("../../prompts/responses", () => ({ + formatResponse: { + toolResult: jest.fn((result: string) => `Tool result: ${result}`), + toolError: jest.fn((error: string) => `Tool error: ${error}`), + invalidMcpToolArgumentError: jest.fn((server: string, tool: string) => `Invalid args for ${server}:${tool}`), + }, +})) + +describe("useMcpToolTool", () => { + let mockTask: Partial + let mockAskApproval: jest.Mock + let mockHandleError: jest.Mock + let mockPushToolResult: jest.Mock + let mockRemoveClosingTag: jest.Mock + let mockProviderRef: any + + beforeEach(() => { + mockAskApproval = jest.fn() + mockHandleError = jest.fn() + mockPushToolResult = jest.fn() + mockRemoveClosingTag = jest.fn((tag: string, value?: string) => value || "") + + mockProviderRef = { + deref: jest.fn().mockReturnValue({ + getMcpHub: jest.fn().mockReturnValue({ + callTool: jest.fn(), + }), + postMessageToWebview: jest.fn(), + }), + } + + mockTask = { + consecutiveMistakeCount: 0, + recordToolError: jest.fn(), + sayAndCreateMissingParamError: jest.fn(), + say: jest.fn(), + ask: jest.fn(), + lastMessageTs: 123456789, + providerRef: mockProviderRef, + } + }) + + describe("parameter validation", () => { + it("should handle missing server_name", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + tool_name: "test_tool", + arguments: "{}", + }, + partial: false, + } + + mockTask.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing server_name error") + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool") + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("use_mcp_tool", "server_name") + expect(mockPushToolResult).toHaveBeenCalledWith("Missing server_name error") + }) + + it("should handle missing tool_name", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + arguments: "{}", + }, + partial: false, + } + + mockTask.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing tool_name error") + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool") + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("use_mcp_tool", "tool_name") + expect(mockPushToolResult).toHaveBeenCalledWith("Missing tool_name error") + }) + + it("should handle invalid JSON arguments", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + arguments: "invalid json", + }, + partial: false, + } + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool") + expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument")) + expect(mockPushToolResult).toHaveBeenCalledWith("Tool error: Invalid args for test_server:test_tool") + }) + }) + + describe("partial requests", () => { + it("should handle partial requests", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + arguments: "{}", + }, + partial: true, + } + + mockTask.ask = jest.fn().mockResolvedValue(true) + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.ask).toHaveBeenCalledWith("use_mcp_server", expect.stringContaining("use_mcp_tool"), true) + }) + }) + + describe("successful execution", () => { + it("should execute tool successfully with valid parameters", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + arguments: '{"param": "value"}', + }, + partial: false, + } + + mockAskApproval.mockResolvedValue(true) + + const mockToolResult = { + content: [{ type: "text", text: "Tool executed successfully" }], + isError: false, + } + + mockProviderRef.deref.mockReturnValue({ + getMcpHub: () => ({ + callTool: jest.fn().mockResolvedValue(mockToolResult), + }), + postMessageToWebview: jest.fn(), + }) + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started") + expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully") + expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully") + }) + + it("should handle user rejection", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + arguments: "{}", + }, + partial: false, + } + + mockAskApproval.mockResolvedValue(false) + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).not.toHaveBeenCalledWith("mcp_server_request_started") + expect(mockPushToolResult).not.toHaveBeenCalled() + }) + }) + + describe("error handling", () => { + it("should handle unexpected errors", async () => { + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + }, + partial: false, + } + + const error = new Error("Unexpected error") + mockAskApproval.mockRejectedValue(error) + + await useMcpToolTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockHandleError).toHaveBeenCalledWith("executing MCP tool", error) + }) + }) +}) diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts index b8339bc8d0..756b6be2a8 100644 --- a/src/core/tools/useMcpToolTool.ts +++ b/src/core/tools/useMcpToolTool.ts @@ -2,6 +2,166 @@ import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" +import { McpExecutionStatus } from "@roo-code/types" + +interface McpToolParams { + server_name?: string + tool_name?: string + arguments?: string +} + +type ValidationResult = + | { isValid: false } + | { + isValid: true + serverName: string + toolName: string + parsedArguments?: Record + } + +async function handlePartialRequest( + cline: Task, + params: McpToolParams, + removeClosingTag: RemoveClosingTag, +): Promise { + const partialMessage = JSON.stringify({ + type: "use_mcp_tool", + serverName: removeClosingTag("server_name", params.server_name), + toolName: removeClosingTag("tool_name", params.tool_name), + arguments: removeClosingTag("arguments", params.arguments), + } satisfies ClineAskUseMcpServer) + + await cline.ask("use_mcp_server", partialMessage, true).catch(() => {}) +} + +async function validateParams( + cline: Task, + params: McpToolParams, + pushToolResult: PushToolResult, +): Promise { + if (!params.server_name) { + cline.consecutiveMistakeCount++ + cline.recordToolError("use_mcp_tool") + pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name")) + return { isValid: false } + } + + if (!params.tool_name) { + cline.consecutiveMistakeCount++ + cline.recordToolError("use_mcp_tool") + pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name")) + return { isValid: false } + } + + let parsedArguments: Record | undefined + + if (params.arguments) { + try { + parsedArguments = JSON.parse(params.arguments) + } catch (error) { + cline.consecutiveMistakeCount++ + cline.recordToolError("use_mcp_tool") + await cline.say("error", `Roo tried to use ${params.tool_name} with an invalid JSON argument. Retrying...`) + + pushToolResult( + formatResponse.toolError( + formatResponse.invalidMcpToolArgumentError(params.server_name, params.tool_name), + ), + ) + return { isValid: false } + } + } + + return { + isValid: true, + serverName: params.server_name, + toolName: params.tool_name, + parsedArguments, + } +} + +async function sendExecutionStatus(cline: Task, status: McpExecutionStatus): Promise { + const clineProvider = await cline.providerRef.deref() + clineProvider?.postMessageToWebview({ + type: "mcpExecutionStatus", + text: JSON.stringify(status), + }) +} + +function processToolContent(toolResult: any): string { + if (!toolResult?.content || toolResult.content.length === 0) { + return "" + } + + return toolResult.content + .map((item: any) => { + if (item.type === "text") { + return item.text + } + if (item.type === "resource") { + const { blob: _, ...rest } = item.resource + return JSON.stringify(rest, null, 2) + } + return "" + }) + .filter(Boolean) + .join("\n\n") +} + +async function executeToolAndProcessResult( + cline: Task, + serverName: string, + toolName: string, + parsedArguments: Record | undefined, + executionId: string, + pushToolResult: PushToolResult, +): Promise { + await cline.say("mcp_server_request_started") + + // Send started status + await sendExecutionStatus(cline, { + executionId, + status: "started", + serverName, + toolName, + }) + + const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments) + + let toolResultPretty = "(No response)" + + if (toolResult) { + const outputText = processToolContent(toolResult) + + if (outputText) { + await sendExecutionStatus(cline, { + executionId, + status: "output", + response: outputText, + }) + + toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText + } + + // Send completion status + await sendExecutionStatus(cline, { + executionId, + status: toolResult.isError ? "error" : "completed", + response: toolResultPretty, + error: toolResult.isError ? "Error executing MCP tool" : undefined, + }) + } else { + // Send error status if no result + await sendExecutionStatus(cline, { + executionId, + status: "error", + error: "No response from MCP server", + }) + } + + await cline.say("mcp_server_response", toolResultPretty) + pushToolResult(formatResponse.toolResult(toolResultPretty)) +} export async function useMcpToolTool( cline: Task, @@ -11,100 +171,48 @@ export async function useMcpToolTool( pushToolResult: PushToolResult, removeClosingTag: RemoveClosingTag, ) { - const server_name: string | undefined = block.params.server_name - const tool_name: string | undefined = block.params.tool_name - const mcp_arguments: string | undefined = block.params.arguments try { + const params: McpToolParams = { + server_name: block.params.server_name, + tool_name: block.params.tool_name, + arguments: block.params.arguments, + } + + // Handle partial requests if (block.partial) { - const partialMessage = JSON.stringify({ - type: "use_mcp_tool", - serverName: removeClosingTag("server_name", server_name), - toolName: removeClosingTag("tool_name", tool_name), - arguments: removeClosingTag("arguments", mcp_arguments), - } satisfies ClineAskUseMcpServer) - - await cline.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) + await handlePartialRequest(cline, params, removeClosingTag) return - } else { - if (!server_name) { - cline.consecutiveMistakeCount++ - cline.recordToolError("use_mcp_tool") - pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name")) - return - } - - if (!tool_name) { - cline.consecutiveMistakeCount++ - cline.recordToolError("use_mcp_tool") - pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name")) - return - } - - let parsedArguments: Record | undefined - - if (mcp_arguments) { - try { - parsedArguments = JSON.parse(mcp_arguments) - } catch (error) { - cline.consecutiveMistakeCount++ - cline.recordToolError("use_mcp_tool") - await cline.say("error", `Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`) - - pushToolResult( - formatResponse.toolError(formatResponse.invalidMcpToolArgumentError(server_name, tool_name)), - ) - - return - } - } + } - cline.consecutiveMistakeCount = 0 + // Validate parameters + const validation = await validateParams(cline, params, pushToolResult) + if (!validation.isValid) { + return + } - const completeMessage = JSON.stringify({ - type: "use_mcp_tool", - serverName: server_name, - toolName: tool_name, - arguments: mcp_arguments, - } satisfies ClineAskUseMcpServer) + const { serverName, toolName, parsedArguments } = validation - const didApprove = await askApproval("use_mcp_server", completeMessage) + // Reset mistake count on successful validation + cline.consecutiveMistakeCount = 0 - if (!didApprove) { - return - } + // Get user approval + const completeMessage = JSON.stringify({ + type: "use_mcp_tool", + serverName, + toolName, + arguments: params.arguments, + } satisfies ClineAskUseMcpServer) - // Now execute the tool - await cline.say("mcp_server_request_started") // same as browser_action_result - - const toolResult = await cline.providerRef - .deref() - ?.getMcpHub() - ?.callTool(server_name, tool_name, parsedArguments) - - // TODO: add progress indicator and ability to parse images and non-text responses - const toolResultPretty = - (toolResult?.isError ? "Error:\n" : "") + - toolResult?.content - .map((item) => { - if (item.type === "text") { - return item.text - } - if (item.type === "resource") { - const { blob: _, ...rest } = item.resource - return JSON.stringify(rest, null, 2) - } - return "" - }) - .filter(Boolean) - .join("\n\n") || "(No response)" - - await cline.say("mcp_server_response", toolResultPretty) - pushToolResult(formatResponse.toolResult(toolResultPretty)) + const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString() + const didApprove = await askApproval("use_mcp_server", completeMessage) + if (!didApprove) { return } + + // Execute the tool and process results + await executeToolAndProcessResult(cline, serverName!, toolName!, parsedArguments, executionId, pushToolResult) } catch (error) { await handleError("executing MCP tool", error) - return } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 739815be5a..95729a6bea 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -67,6 +67,7 @@ export interface ExtensionMessage { | "acceptInput" | "setHistoryPreviewCollapsed" | "commandExecutionStatus" + | "mcpExecutionStatus" | "vsCodeSetting" | "authenticatedUser" | "condenseTaskContextResponse" @@ -313,6 +314,7 @@ export interface ClineAskUseMcpServer { toolName?: string arguments?: string uri?: string + response?: string } export interface ClineApiReqInfo { diff --git a/src/shared/__tests__/combineCommandSequences.test.ts b/src/shared/__tests__/combineCommandSequences.test.ts index 93305b20ce..c3e9644408 100644 --- a/src/shared/__tests__/combineCommandSequences.test.ts +++ b/src/shared/__tests__/combineCommandSequences.test.ts @@ -1,47 +1,183 @@ -// npx jest src/shared/__tests__/combineCommandSequences.test.ts +import { combineCommandSequences } from "../combineCommandSequences" +import { ClineMessage } from "@roo-code/types" -import type { ClineMessage } from "@roo-code/types" +describe("combineCommandSequences", () => { + describe("command sequences", () => { + it("should combine command and command_output messages", () => { + const messages: ClineMessage[] = [ + { type: "ask", ask: "command", text: "ls", ts: 1625097600000 }, + { type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 }, + { type: "ask", ask: "command_output", text: "file2.txt", ts: 1625097602000 }, + ] -import { combineCommandSequences } from "../combineCommandSequences" + const result = combineCommandSequences(messages) -const messages: ClineMessage[] = [ - { - ts: 1745710928469, - type: "say", - say: "api_req_started", - text: '{"request":"\\nRun the command \\"ping w…tes":12117,"cacheReads":0,"cost":0.020380125}', - images: undefined, - }, - { - ts: 1745710930332, - type: "say", - say: "text", - text: "Okay, I can run that command for you. The `pin…'s reachable and measure the round-trip time.", - images: undefined, - }, - { ts: 1745710930748, type: "ask", ask: "command", text: "ping www.google.com", partial: false }, - { ts: 1745710930894, type: "say", say: "command_output", text: "", images: undefined }, - { ts: 1745710930894, type: "ask", ask: "command_output", text: "" }, - { - ts: 1745710930954, - type: "say", - say: "command_output", - text: "PING www.google.com (142.251.46.228): 56 data bytes\n", - images: undefined, - }, - { - ts: 1745710930954, - type: "ask", - ask: "command_output", - text: "PING www.google.com (142.251.46.228): 56 data bytes\n", - }, -] + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + type: "ask", + ask: "command", + text: "ls\nOutput:file1.txt\nfile2.txt", + ts: 1625097600000, + }) + }) + }) -describe("combineCommandSequences", () => { - it("should combine command sequences", () => { - const message = combineCommandSequences(messages).at(-1) - expect(message!.text).toEqual( - "ping www.google.com\nOutput:PING www.google.com (142.251.46.228): 56 data bytes\n", - ) + describe("MCP server responses", () => { + it("should combine use_mcp_server and mcp_server_response messages", () => { + const messages: ClineMessage[] = [ + { + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + }), + ts: 1625097600000, + }, + { type: "say", say: "mcp_server_response", text: "Response data", ts: 1625097601000 }, + ] + + const result = combineCommandSequences(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + response: "Response data", + }), + ts: 1625097600000, + }) + }) + + it("should handle multiple mcp_server_response messages", () => { + const messages: ClineMessage[] = [ + { + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + }), + ts: 1625097600000, + }, + { type: "say", say: "mcp_server_response", text: "First response", ts: 1625097601000 }, + { type: "say", say: "mcp_server_response", text: "Second response", ts: 1625097602000 }, + ] + + const result = combineCommandSequences(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + response: "First response\nSecond response", + }), + ts: 1625097600000, + }) + }) + + it("should handle multiple MCP server requests", () => { + const messages: ClineMessage[] = [ + { + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server-1", + toolName: "test-tool-1", + arguments: { param: "value1" }, + }), + ts: 1625097600000, + }, + { type: "say", say: "mcp_server_response", text: "Response 1", ts: 1625097601000 }, + { + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server-2", + toolName: "test-tool-2", + arguments: { param: "value2" }, + }), + ts: 1625097602000, + }, + { type: "say", say: "mcp_server_response", text: "Response 2", ts: 1625097603000 }, + ] + + const result = combineCommandSequences(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server-1", + toolName: "test-tool-1", + arguments: { param: "value1" }, + response: "Response 1", + }), + ts: 1625097600000, + }) + expect(result[1]).toEqual({ + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server-2", + toolName: "test-tool-2", + arguments: { param: "value2" }, + response: "Response 2", + }), + ts: 1625097602000, + }) + }) + }) + + describe("mixed sequences", () => { + it("should handle both command and MCP server sequences", () => { + const messages: ClineMessage[] = [ + { type: "ask", ask: "command", text: "ls", ts: 1625097600000 }, + { type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 }, + { + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + }), + ts: 1625097602000, + }, + { type: "say", say: "mcp_server_response", text: "MCP response", ts: 1625097603000 }, + ] + + const result = combineCommandSequences(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + type: "ask", + ask: "command", + text: "ls\nOutput:file1.txt", + ts: 1625097600000, + }) + expect(result[1]).toEqual({ + type: "ask", + ask: "use_mcp_server", + text: JSON.stringify({ + serverName: "test-server", + toolName: "test-tool", + arguments: { param: "value" }, + response: "MCP response", + }), + ts: 1625097602000, + }) + }) }) }) diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts index 7b37b72c63..d883e83d45 100644 --- a/src/shared/combineCommandSequences.ts +++ b/src/shared/combineCommandSequences.ts @@ -1,17 +1,20 @@ -import type { ClineMessage } from "@roo-code/types" +import { ClineMessage } from "@roo-code/types" +import { safeJsonParse } from "./safeJsonParse" export const COMMAND_OUTPUT_STRING = "Output:" /** * Combines sequences of command and command_output messages in an array of ClineMessages. + * Also combines sequences of use_mcp_server and mcp_server_response messages. * * This function processes an array of ClineMessages objects, looking for sequences - * where a 'command' message is followed by one or more 'command_output' messages. + * where a 'command' message is followed by one or more 'command_output' messages, + * or where a 'use_mcp_server' message is followed by one or more 'mcp_server_response' messages. * When such a sequence is found, it combines them into a single message, merging * their text contents. * * @param messages - An array of ClineMessage objects to process. - * @returns A new array of ClineMessage objects with command sequences combined. + * @returns A new array of ClineMessage objects with command and MCP sequences combined. * * @example * const messages: ClineMessage[] = [ @@ -24,8 +27,54 @@ export const COMMAND_OUTPUT_STRING = "Output:" */ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] { const combinedCommands: ClineMessage[] = [] + const combinedMcpResponses: ClineMessage[] = [] - // First pass: combine commands with their outputs. + // Create a map of MCP server responses by timestamp + const mcpResponseMap = new Map() + + // First, collect all MCP server responses + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] + if (msg.say === "mcp_server_response") { + // Find the closest preceding use_mcp_server message + let j = i - 1 + while (j >= 0) { + if (messages[j].type === "ask" && messages[j].ask === "use_mcp_server") { + const ts = messages[j].ts + const currentResponse = mcpResponseMap.get(ts) || "" + const newResponse = currentResponse ? currentResponse + "\n" + (msg.text || "") : msg.text || "" + mcpResponseMap.set(ts, newResponse) + break + } + j-- + } + } + } + + // Process all MCP server requests first + for (let i = 0; i < messages.length; i++) { + if (messages[i].type === "ask" && messages[i].ask === "use_mcp_server") { + const mcpResponse = mcpResponseMap.get(messages[i].ts) + + if (mcpResponse) { + // Parse the JSON from the message text + const jsonObj = safeJsonParse(messages[i].text || "{}", {}) + + // Add the response to the JSON object + jsonObj.response = mcpResponse + + // Stringify the updated JSON object + const combinedText = JSON.stringify(jsonObj) + + combinedMcpResponses.push({ ...messages[i], text: combinedText }) + } else { + // If there's no response, just keep the original message + combinedMcpResponses.push({ ...messages[i] }) + } + } + } + + // Then process command sequences for (let i = 0; i < messages.length; i++) { if (messages[i].type === "ask" && messages[i].ask === "command") { let combinedText = messages[i].text || "" @@ -47,6 +96,14 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[ const isDuplicate = previous && previous.type !== type && previous.text === text if (text.length > 0 && !isDuplicate) { + // Add a newline before adding the text if there's already content + if ( + previous && + combinedText.length > + combinedText.indexOf(COMMAND_OUTPUT_STRING) + COMMAND_OUTPUT_STRING.length + ) { + combinedText += "\n" + } combinedText += text } @@ -63,17 +120,23 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[ } } - // console.log(`[combineCommandSequences] combinedCommands ->`, messages, combinedCommands) - - // Second pass: remove command_outputs and replace original commands with + // Second pass: remove command_outputs and mcp_server_responses, and replace original commands and MCP requests with // combined ones. - return messages - .filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output")) + const result = messages + .filter( + (msg) => + !(msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response"), + ) .map((msg) => { if (msg.type === "ask" && msg.ask === "command") { return combinedCommands.find((cmd) => cmd.ts === msg.ts) || msg } + if (msg.type === "ask" && msg.ask === "use_mcp_server") { + return combinedMcpResponses.find((mcp) => mcp.ts === msg.ts) || msg + } return msg }) + + return result } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index cc351343b5..bffcb0f375 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1,4 +1,5 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" +import { McpExecution } from "./McpExecution" import { useSize } from "react-use" import { useTranslation, Trans } from "react-i18next" import deepEqual from "fast-deep-equal" @@ -25,7 +26,6 @@ import MarkdownBlock from "../common/MarkdownBlock" import { ReasoningBlock } from "./ReasoningBlock" import Thumbnails from "../common/Thumbnails" import McpResourceRow from "../mcp/McpResourceRow" -import McpToolRow from "../mcp/McpToolRow" import { Mention } from "./Mention" import { CheckpointSaved } from "./checkpoints/CheckpointSaved" @@ -965,28 +965,7 @@ export const ChatRowContent = ({ ) case "shell_integration_warning": return - case "mcp_server_response": - return ( - <> -
-
- {t("chat:response")} -
- -
- - ) + return null case "checkpoint_saved": return ( ) case "use_mcp_server": - const useMcpServer = safeJsonParse(message.text) + // Parse the message text to get the MCP server request + const messageJson = safeJsonParse(message.text, {}) + + // Extract the response field if it exists + const { response, ...mcpServerRequest } = messageJson + + // Create the useMcpServer object with the response field + const useMcpServer: ClineAskUseMcpServer = { + ...mcpServerRequest, + response, + } if (!useMcpServer) { return null @@ -1084,13 +1073,7 @@ export const ChatRowContent = ({ {icon} {title} -
+
{useMcpServer.type === "access_mcp_resource" && ( )} {useMcpServer.type === "use_mcp_tool" && ( - <> -
e.stopPropagation()}> - tool.name === useMcpServer.toolName, - )?.description || "", - alwaysAllow: - server?.tools?.find( - (tool) => tool.name === useMcpServer.toolName, - )?.alwaysAllow || false, - }} - serverName={useMcpServer.serverName} - serverSource={server?.source} - alwaysAllowMcp={alwaysAllowMcp} - /> -
- {useMcpServer.arguments && useMcpServer.arguments !== "{}" && ( -
-
- {t("chat:arguments")} -
- -
- )} - + )}
diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx new file mode 100644 index 0000000000..eed45690a3 --- /dev/null +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -0,0 +1,280 @@ +import { useCallback, useEffect, useMemo, useState, memo } from "react" +import { Server, ChevronDown } from "lucide-react" +import { useEvent } from "react-use" + +import { McpExecutionStatus, mcpExecutionStatusSchema } from "@roo-code/types" +import { ExtensionMessage, ClineAskUseMcpServer } from "../../../../src/shared/ExtensionMessage" +import { safeJsonParse } from "../../../../src/shared/safeJsonParse" +import { cn } from "@src/lib/utils" +import { Button } from "@src/components/ui" +import CodeBlock from "../common/CodeBlock" +import McpToolRow from "../mcp/McpToolRow" + +interface McpExecutionProps { + executionId: string + text?: string + serverName?: string + toolName?: string + isArguments?: boolean + server?: { + tools?: Array<{ + name: string + description?: string + alwaysAllow?: boolean + }> + source?: "global" | "project" + } + useMcpServer?: ClineAskUseMcpServer + alwaysAllowMcp?: boolean +} + +export const McpExecution = ({ + executionId, + text, + serverName: initialServerName, + toolName: initialToolName, + isArguments = false, + server, + useMcpServer, + alwaysAllowMcp = false, +}: McpExecutionProps) => { + // State for tracking MCP response status + const [status, setStatus] = useState(null) + const [responseText, setResponseText] = useState(text || "") + const [argumentsText, setArgumentsText] = useState(text || "") + const [serverName, setServerName] = useState(initialServerName) + const [toolName, setToolName] = useState(initialToolName) + + // Only need expanded state for response section (like command output) + const [isResponseExpanded, setIsResponseExpanded] = useState(false) + + // Try to parse the text as JSON for proper formatting + const formatJsonText = (inputText: string) => { + if (!inputText) return "" + + try { + // If it's already valid JSON, pretty-print it + const parsed = safeJsonParse(inputText, null) + if (parsed !== null) { + return JSON.stringify(parsed, null, 2) + } + return inputText + } catch (_e) { + // If parsing fails, return the original text + return inputText + } + } + + const formattedResponseText = useMemo(() => formatJsonText(responseText), [responseText]) + const formattedArgumentsText = useMemo(() => formatJsonText(argumentsText), [argumentsText]) + + const onToggleResponseExpand = useCallback(() => { + setIsResponseExpanded(!isResponseExpanded) + }, [isResponseExpanded]) + + // Listen for MCP execution status messages + const onMessage = useCallback( + (event: MessageEvent) => { + const message: ExtensionMessage = event.data + + if (message.type === "mcpExecutionStatus") { + try { + const result = mcpExecutionStatusSchema.safeParse(safeJsonParse(message.text || "{}", {})) + + if (result.success) { + const data = result.data + + // Only update if this message is for our response + if (data.executionId === executionId) { + setStatus(data) + + if (data.status === "output" && data.response) { + setResponseText((prev) => prev + data.response) + // Keep the arguments when we get output + if (isArguments && argumentsText === responseText) { + setArgumentsText(responseText) + } + } else if (data.status === "completed" && data.response) { + setResponseText(data.response) + // Keep the arguments when we get completed response + if (isArguments && argumentsText === responseText) { + setArgumentsText(responseText) + } + } + } + } + } catch (e) { + console.error("Failed to parse MCP execution status", e) + } + } + }, + [argumentsText, executionId, isArguments, responseText], + ) + + useEvent("message", onMessage) + + // Initialize with text if provided and parse command/response sections + useEffect(() => { + // Handle arguments text + if (text) { + try { + // Try to parse the text as JSON for arguments + const jsonObj = safeJsonParse(text, null) + + if (jsonObj && typeof jsonObj === "object") { + // Format the JSON for display + setArgumentsText(JSON.stringify(jsonObj, null, 2)) + } else { + // If not valid JSON, use as is + setArgumentsText(text) + } + } catch (_e) { + // If parsing fails, use text as is + setArgumentsText(text) + } + } + + // Handle response text + if (useMcpServer?.response) { + setResponseText(useMcpServer.response) + } + + if (initialServerName && initialServerName !== serverName) { + setServerName(initialServerName) + } + + if (initialToolName && initialToolName !== toolName) { + setToolName(initialToolName) + } + }, [text, useMcpServer, initialServerName, initialToolName, serverName, toolName, isArguments]) + + return ( + <> +
+
+ +
+ {serverName && {serverName}} + {serverName && toolName && } + {toolName && {toolName}} +
+
+
+
+ {status && ( +
+
+
+ {status.status === "started" + ? "Running" + : status.status === "completed" + ? "Completed" + : "Error"} +
+ {status.status === "error" && "error" in status && status.error && ( +
({status.error})
+ )} +
+ )} + {responseText && responseText.length > 0 && ( + + )} +
+
+
+ +
+ {/* Tool information section */} + {useMcpServer?.type === "use_mcp_tool" && ( +
e.stopPropagation()}> + tool.name === useMcpServer.toolName)?.description || + "", + alwaysAllow: + server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.alwaysAllow || + false, + }} + serverName={useMcpServer.serverName} + serverSource={server?.source} + alwaysAllowMcp={alwaysAllowMcp} + /> +
+ )} + {!useMcpServer && toolName && serverName && ( +
e.stopPropagation()}> + +
+ )} + + {/* Arguments section - display like command (always visible) */} + {(isArguments || useMcpServer?.arguments || argumentsText) && ( +
+ +
+ )} + + {/* Response section - collapsible like command output */} + +
+ + ) +} + +McpExecution.displayName = "McpExecution" + +const ResponseContainerInternal = ({ + isExpanded, + response, + hasArguments, +}: { + isExpanded: boolean + response: string + hasArguments?: boolean +}) => ( +
+ {response.length > 0 && } +
+) + +const ResponseContainer = memo(ResponseContainerInternal) diff --git a/webview-ui/src/components/common/CodeAccordian.tsx b/webview-ui/src/components/common/CodeAccordian.tsx index b42d0e2f7e..b07461c70e 100644 --- a/webview-ui/src/components/common/CodeAccordian.tsx +++ b/webview-ui/src/components/common/CodeAccordian.tsx @@ -1,8 +1,6 @@ import { memo, useMemo } from "react" import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" - -import type { ToolProgressStatus } from "@roo-code/types" - +import { type ToolProgressStatus } from "@roo-code/types" import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric" @@ -18,6 +16,7 @@ interface CodeAccordianProps { isExpanded: boolean isFeedback?: boolean onToggleExpand: () => void + header?: string } const CodeAccordian = ({ @@ -29,17 +28,23 @@ const CodeAccordian = ({ isExpanded, isFeedback, onToggleExpand, + header, }: CodeAccordianProps) => { const inferredLanguage = useMemo(() => language ?? (path ? getLanguageFromPath(path) : "txt"), [path, language]) const source = useMemo(() => code.trim(), [code]) - const hasHeader = Boolean(path || isFeedback) + const hasHeader = Boolean(path || isFeedback || header) return ( {hasHeader && ( {isLoading && } - {isFeedback ? ( + {header ? ( +
+ + {header} +
+ ) : isFeedback ? (
@@ -75,6 +80,4 @@ const CodeAccordian = ({ ) } -// Memo does shallow comparison of props, so if you need it to re-render when a -// nested object changes, you need to pass a custom comparison function. export default memo(CodeAccordian) From 3992301d0aed9283dc4e7d70bbc65718f9040229 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 5 Jun 2025 09:34:27 -0500 Subject: [PATCH 2/8] refactor: merge loops in combineCommandSequences for better efficiency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduced from 3 separate loops to 1 main processing loop - Improved time complexity from O(n²) to O(n) - Reduced total passes through array from 4 to 2 - Better memory access patterns and cache utilization --- src/shared/combineCommandSequences.ts | 109 +++++++++++++------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts index d883e83d45..2f655feb54 100644 --- a/src/shared/combineCommandSequences.ts +++ b/src/shared/combineCommandSequences.ts @@ -26,60 +26,54 @@ export const COMMAND_OUTPUT_STRING = "Output:" * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }] */ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] { - const combinedCommands: ClineMessage[] = [] - const combinedMcpResponses: ClineMessage[] = [] + const combinedMessages = new Map() + const processedIndices = new Set() - // Create a map of MCP server responses by timestamp - const mcpResponseMap = new Map() - - // First, collect all MCP server responses + // Single pass through all messages for (let i = 0; i < messages.length; i++) { const msg = messages[i] - if (msg.say === "mcp_server_response") { - // Find the closest preceding use_mcp_server message - let j = i - 1 - while (j >= 0) { - if (messages[j].type === "ask" && messages[j].ask === "use_mcp_server") { - const ts = messages[j].ts - const currentResponse = mcpResponseMap.get(ts) || "" - const newResponse = currentResponse ? currentResponse + "\n" + (msg.text || "") : msg.text || "" - mcpResponseMap.set(ts, newResponse) + + // Handle MCP server requests + if (msg.type === "ask" && msg.ask === "use_mcp_server") { + // Look ahead for MCP responses + let responses: string[] = [] + let j = i + 1 + + while (j < messages.length) { + if (messages[j].say === "mcp_server_response") { + responses.push(messages[j].text || "") + processedIndices.add(j) + j++ + } else if (messages[j].type === "ask" && messages[j].ask === "use_mcp_server") { + // Stop if we encounter another MCP request break + } else { + j++ } - j-- } - } - } - - // Process all MCP server requests first - for (let i = 0; i < messages.length; i++) { - if (messages[i].type === "ask" && messages[i].ask === "use_mcp_server") { - const mcpResponse = mcpResponseMap.get(messages[i].ts) - if (mcpResponse) { + if (responses.length > 0) { // Parse the JSON from the message text - const jsonObj = safeJsonParse(messages[i].text || "{}", {}) + const jsonObj = safeJsonParse(msg.text || "{}", {}) // Add the response to the JSON object - jsonObj.response = mcpResponse + jsonObj.response = responses.join("\n") // Stringify the updated JSON object const combinedText = JSON.stringify(jsonObj) - combinedMcpResponses.push({ ...messages[i], text: combinedText }) + combinedMessages.set(msg.ts, { ...msg, text: combinedText }) } else { // If there's no response, just keep the original message - combinedMcpResponses.push({ ...messages[i] }) + combinedMessages.set(msg.ts, { ...msg }) } } - } - - // Then process command sequences - for (let i = 0; i < messages.length; i++) { - if (messages[i].type === "ask" && messages[i].ask === "command") { - let combinedText = messages[i].text || "" + // Handle command sequences + else if (msg.type === "ask" && msg.ask === "command") { + let combinedText = msg.text || "" let j = i + 1 let previous: { type: "ask" | "say"; text: string } | undefined + let lastProcessedIndex = i while (j < messages.length) { const { type, ask, say, text = "" } = messages[j] @@ -108,35 +102,44 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[ } previous = { type, text } + processedIndices.add(j) + lastProcessedIndex = j } j++ } - combinedCommands.push({ ...messages[i], text: combinedText }) + combinedMessages.set(msg.ts, { ...msg, text: combinedText }) - // Move to the index just before the next command or end of array. - i = j - 1 + // Only skip ahead if we actually processed command outputs + if (lastProcessedIndex > i) { + i = lastProcessedIndex + } } } - // Second pass: remove command_outputs and mcp_server_responses, and replace original commands and MCP requests with - // combined ones. - const result = messages - .filter( - (msg) => - !(msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response"), - ) - .map((msg) => { - if (msg.type === "ask" && msg.ask === "command") { - return combinedCommands.find((cmd) => cmd.ts === msg.ts) || msg - } - if (msg.type === "ask" && msg.ask === "use_mcp_server") { - return combinedMcpResponses.find((mcp) => mcp.ts === msg.ts) || msg - } + // Build final result: filter out processed messages and use combined versions + const result: ClineMessage[] = [] + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] - return msg - }) + // Skip messages that were processed as outputs/responses + if (processedIndices.has(i)) { + continue + } + + // Skip command_output and mcp_server_response messages + if (msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response") { + continue + } + + // Use combined version if available + if (combinedMessages.has(msg.ts)) { + result.push(combinedMessages.get(msg.ts)!) + } else { + result.push(msg) + } + } return result } From c54244e457a835702b430af1828240a0496a63ee Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 6 Jun 2025 08:32:17 -0500 Subject: [PATCH 3/8] feat: enhance JSON parsing and rendering in McpExecution component --- .../src/components/chat/McpExecution.tsx | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index eed45690a3..d95986539a 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -9,6 +9,7 @@ import { cn } from "@src/lib/utils" import { Button } from "@src/components/ui" import CodeBlock from "../common/CodeBlock" import McpToolRow from "../mcp/McpToolRow" +import { Markdown } from "../ui/markdown" interface McpExecutionProps { executionId: string @@ -48,25 +49,30 @@ export const McpExecution = ({ // Only need expanded state for response section (like command output) const [isResponseExpanded, setIsResponseExpanded] = useState(false) - // Try to parse the text as JSON for proper formatting - const formatJsonText = (inputText: string) => { - if (!inputText) return "" + // Try to parse JSON and return both the result and formatted text + const tryParseJson = (text: string): { isJson: boolean; formatted: string } => { + if (!text) return { isJson: false, formatted: "" } try { - // If it's already valid JSON, pretty-print it - const parsed = safeJsonParse(inputText, null) - if (parsed !== null) { - return JSON.stringify(parsed, null, 2) + const parsed = JSON.parse(text) + return { + isJson: true, + formatted: JSON.stringify(parsed, null, 2), + } + } catch { + return { + isJson: false, + formatted: text, } - return inputText - } catch (_e) { - // If parsing fails, return the original text - return inputText } } - const formattedResponseText = useMemo(() => formatJsonText(responseText), [responseText]) - const formattedArgumentsText = useMemo(() => formatJsonText(argumentsText), [argumentsText]) + const responseData = useMemo(() => tryParseJson(responseText), [responseText]) + const argumentsData = useMemo(() => tryParseJson(argumentsText), [argumentsText]) + + const formattedResponseText = responseData.formatted + const formattedArgumentsText = argumentsData.formatted + const responseIsJson = responseData.isJson const onToggleResponseExpand = useCallback(() => { setIsResponseExpanded(!isResponseExpanded) @@ -249,6 +255,7 @@ export const McpExecution = ({
@@ -261,10 +268,12 @@ McpExecution.displayName = "McpExecution" const ResponseContainerInternal = ({ isExpanded, response, + isJson, hasArguments, }: { isExpanded: boolean response: string + isJson: boolean hasArguments?: boolean }) => (
- {response.length > 0 && } + {response.length > 0 && + (isJson ? : )}
) From a62f37e9081bd0796131752055b4976579b9f0ed Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 6 Jun 2025 09:02:07 -0500 Subject: [PATCH 4/8] feat: add mock implementations for react-markdown and remark-gfm --- webview-ui/jest.config.cjs | 2 ++ webview-ui/src/__mocks__/react-markdown.tsx | 19 +++++++++++++++++++ webview-ui/src/__mocks__/remark-gfm.ts | 3 +++ 3 files changed, 24 insertions(+) create mode 100644 webview-ui/src/__mocks__/react-markdown.tsx create mode 100644 webview-ui/src/__mocks__/remark-gfm.ts diff --git a/webview-ui/jest.config.cjs b/webview-ui/jest.config.cjs index 2a7374fe76..367bbff3c7 100644 --- a/webview-ui/jest.config.cjs +++ b/webview-ui/jest.config.cjs @@ -22,6 +22,8 @@ module.exports = { "^\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", "^@src/utils/highlighter$": "/src/__mocks__/utils/highlighter.ts", "^shiki$": "/src/__mocks__/shiki.ts", + "^react-markdown$": "/src/__mocks__/react-markdown.tsx", + "^remark-gfm$": "/src/__mocks__/remark-gfm.ts", }, reporters: [["jest-simple-dot-reporter", {}]], transformIgnorePatterns: [ diff --git a/webview-ui/src/__mocks__/react-markdown.tsx b/webview-ui/src/__mocks__/react-markdown.tsx new file mode 100644 index 0000000000..aed17dfc2f --- /dev/null +++ b/webview-ui/src/__mocks__/react-markdown.tsx @@ -0,0 +1,19 @@ +import React from "react" + +interface ReactMarkdownProps { + children?: React.ReactNode + className?: string + remarkPlugins?: any[] + components?: any +} + +const ReactMarkdown: React.FC = ({ children, className }) => { + return ( +
+ {children} +
+ ) +} + +export default ReactMarkdown +export type { ReactMarkdownProps as Options } diff --git a/webview-ui/src/__mocks__/remark-gfm.ts b/webview-ui/src/__mocks__/remark-gfm.ts new file mode 100644 index 0000000000..3789ec1c55 --- /dev/null +++ b/webview-ui/src/__mocks__/remark-gfm.ts @@ -0,0 +1,3 @@ +const remarkGfm = () => {} + +export default remarkGfm From a48595e15260e7323f06762c1e771ec2522fc9fc Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Sun, 8 Jun 2025 14:14:44 -0500 Subject: [PATCH 5/8] fix: remove unreachable return statement in ChatRowContent component --- webview-ui/src/components/chat/ChatRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index bffcb0f375..0aa91ff917 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -965,7 +965,6 @@ export const ChatRowContent = ({ ) case "shell_integration_warning": return - return null case "checkpoint_saved": return ( Date: Mon, 9 Jun 2025 10:42:17 -0500 Subject: [PATCH 6/8] feat(i18n): add execution status messages and error handling for invalid JSON arguments --- src/core/tools/useMcpToolTool.ts | 3 ++- src/i18n/locales/ca/mcp.json | 3 ++- src/i18n/locales/de/mcp.json | 3 ++- src/i18n/locales/en/mcp.json | 3 ++- src/i18n/locales/es/mcp.json | 3 ++- src/i18n/locales/fr/mcp.json | 4 ++-- src/i18n/locales/hi/mcp.json | 4 ++-- src/i18n/locales/it/mcp.json | 4 ++-- src/i18n/locales/ja/mcp.json | 4 ++-- src/i18n/locales/ko/mcp.json | 4 ++-- src/i18n/locales/nl/mcp.json | 4 ++-- src/i18n/locales/pl/mcp.json | 4 ++-- src/i18n/locales/pt-BR/mcp.json | 4 ++-- src/i18n/locales/ru/mcp.json | 4 ++-- src/i18n/locales/tr/mcp.json | 4 ++-- src/i18n/locales/vi/mcp.json | 4 ++-- src/i18n/locales/zh-CN/mcp.json | 4 ++-- src/i18n/locales/zh-TW/mcp.json | 4 ++-- webview-ui/src/components/chat/McpExecution.tsx | 9 ++++++--- webview-ui/src/i18n/locales/ca/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/de/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/en/mcp.json | 5 +++++ webview-ui/src/i18n/locales/es/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/fr/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/hi/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/it/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/ja/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/ko/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/nl/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/pl/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/pt-BR/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/ru/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/tr/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/vi/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/zh-CN/mcp.json | 7 ++++++- webview-ui/src/i18n/locales/zh-TW/mcp.json | 7 ++++++- 36 files changed, 143 insertions(+), 50 deletions(-) diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts index 756b6be2a8..30dff5ce4f 100644 --- a/src/core/tools/useMcpToolTool.ts +++ b/src/core/tools/useMcpToolTool.ts @@ -3,6 +3,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f import { formatResponse } from "../prompts/responses" import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" import { McpExecutionStatus } from "@roo-code/types" +import { t } from "../../i18n" interface McpToolParams { server_name?: string @@ -61,7 +62,7 @@ async function validateParams( } catch (error) { cline.consecutiveMistakeCount++ cline.recordToolError("use_mcp_tool") - await cline.say("error", `Roo tried to use ${params.tool_name} with an invalid JSON argument. Retrying...`) + await cline.say("error", t("mcp:errors.invalidJsonArgument", { toolName: params.tool_name })) pushToolResult( formatResponse.toolError( diff --git a/src/i18n/locales/ca/mcp.json b/src/i18n/locales/ca/mcp.json index 7b0ee0c344..5bae609960 100644 --- a/src/i18n/locales/ca/mcp.json +++ b/src/i18n/locales/ca/mcp.json @@ -4,7 +4,8 @@ "invalid_settings_syntax": "Format JSON de configuració MCP no vàlid. Si us plau, comprova si hi ha errors de sintaxi al teu fitxer de configuració.", "invalid_settings_validation": "Format de configuració MCP no vàlid: {{errorMessages}}", "create_json": "Ha fallat la creació o obertura de .roo/mcp.json: {{error}}", - "failed_update_project": "Ha fallat l'actualització dels servidors MCP del projecte" + "failed_update_project": "Ha fallat l'actualització dels servidors MCP del projecte", + "invalidJsonArgument": "Roo ha intentat utilitzar {{toolName}} amb un argument JSON no vàlid. Tornant a intentar..." }, "info": { "server_restarting": "Reiniciant el servidor MCP {{serverName}}...", diff --git a/src/i18n/locales/de/mcp.json b/src/i18n/locales/de/mcp.json index 1d4f251d6f..66c25c088f 100644 --- a/src/i18n/locales/de/mcp.json +++ b/src/i18n/locales/de/mcp.json @@ -4,7 +4,8 @@ "invalid_settings_syntax": "Ungültiges MCP-Einstellungen-JSON-Format. Bitte überprüfe deine Einstellungsdatei auf Syntaxfehler.", "invalid_settings_validation": "Ungültiges MCP-Einstellungen-Format: {{errorMessages}}", "create_json": "Fehler beim Erstellen oder Öffnen von .roo/mcp.json: {{error}}", - "failed_update_project": "Fehler beim Aktualisieren der Projekt-MCP-Server" + "failed_update_project": "Fehler beim Aktualisieren der Projekt-MCP-Server", + "invalidJsonArgument": "Roo hat versucht, {{toolName}} mit einem ungültigen JSON-Argument zu verwenden. Wiederhole..." }, "info": { "server_restarting": "MCP-Server {{serverName}} wird neu gestartet...", diff --git a/src/i18n/locales/en/mcp.json b/src/i18n/locales/en/mcp.json index a96baf9ca2..c34b2c9c08 100644 --- a/src/i18n/locales/en/mcp.json +++ b/src/i18n/locales/en/mcp.json @@ -4,7 +4,8 @@ "invalid_settings_syntax": "Invalid MCP settings JSON format. Please check your settings file for syntax errors.", "invalid_settings_validation": "Invalid MCP settings format: {{errorMessages}}", "create_json": "Failed to create or open .roo/mcp.json: {{error}}", - "failed_update_project": "Failed to update project MCP servers" + "failed_update_project": "Failed to update project MCP servers", + "invalidJsonArgument": "Roo tried to use {{toolName}} with an invalid JSON argument. Retrying..." }, "info": { "server_restarting": "Restarting {{serverName}} MCP server...", diff --git a/src/i18n/locales/es/mcp.json b/src/i18n/locales/es/mcp.json index 617baaa5d5..78c41ed556 100644 --- a/src/i18n/locales/es/mcp.json +++ b/src/i18n/locales/es/mcp.json @@ -4,7 +4,8 @@ "invalid_settings_syntax": "Formato JSON de la configuración MCP no válido. Verifica si hay errores de sintaxis en tu archivo de configuración.", "invalid_settings_validation": "Formato de configuración MCP no válido: {{errorMessages}}", "create_json": "Error al crear o abrir .roo/mcp.json: {{error}}", - "failed_update_project": "Error al actualizar los servidores MCP del proyecto" + "failed_update_project": "Error al actualizar los servidores MCP del proyecto", + "invalidJsonArgument": "Roo intentó usar {{toolName}} con un argumento JSON no válido. Reintentando..." }, "info": { "server_restarting": "Reiniciando el servidor MCP {{serverName}}...", diff --git a/src/i18n/locales/fr/mcp.json b/src/i18n/locales/fr/mcp.json index 8beda8a5c3..bbc3eda6b3 100644 --- a/src/i18n/locales/fr/mcp.json +++ b/src/i18n/locales/fr/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Format JSON des paramètres MCP invalide. Veuillez vous assurer que vos paramètres suivent le format JSON correct.", "invalid_settings_syntax": "Format JSON des paramètres MCP invalide. Veuillez vérifier le syntaxe de votre fichier de paramètres.", "invalid_settings_validation": "Format de paramètres MCP invalide : {{errorMessages}}", - "create_json": "Échec de la création ou de l'ouverture de .roo/mcp.json : {{error}}", - "failed_update_project": "Échec de la mise à jour des serveurs MCP du projet" + "failed_update_project": "Échec de la mise à jour des serveurs MCP du projet", + "invalidJsonArgument": "Roo a essayé d'utiliser {{toolName}} avec un argument JSON invalide. Nouvelle tentative..." }, "info": { "server_restarting": "Redémarrage du serveur MCP {{serverName}}...", diff --git a/src/i18n/locales/hi/mcp.json b/src/i18n/locales/hi/mcp.json index 3c6fe57136..e7e0feae0d 100644 --- a/src/i18n/locales/hi/mcp.json +++ b/src/i18n/locales/hi/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही JSON फॉर्मेट का पालन करती हैं।", "invalid_settings_syntax": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया अपनी सेटिंग्स फ़ाइल में सिंटैक्स त्रुटियों की जांच करें।", "invalid_settings_validation": "अमान्य MCP सेटिंग्स फॉर्मेट: {{errorMessages}}", - "create_json": ".roo/mcp.json बनाने या खोलने में विफल: {{error}}", - "failed_update_project": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल" + "failed_update_project": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल", + "invalidJsonArgument": "Roo ने {{toolName}} को अमान्य JSON आर्गुमेंट के साथ उपयोग करने की कोशिश की। फिर से कोशिश कर रहा है..." }, "info": { "server_restarting": "{{serverName}} MCP सर्वर पुनः प्रारंभ हो रहा है...", diff --git a/src/i18n/locales/it/mcp.json b/src/i18n/locales/it/mcp.json index c294ee6afc..1d5a0e39a7 100644 --- a/src/i18n/locales/it/mcp.json +++ b/src/i18n/locales/it/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Formato JSON delle impostazioni MCP non valido. Assicurati che le tue impostazioni seguano il formato JSON corretto.", "invalid_settings_syntax": "Formato JSON delle impostazioni MCP non valido. Verifica gli errori di sintassi nel tuo file delle impostazioni.", "invalid_settings_validation": "Formato delle impostazioni MCP non valido: {{errorMessages}}", - "create_json": "Impossibile creare o aprire .roo/mcp.json: {{error}}", - "failed_update_project": "Errore durante l'aggiornamento dei server MCP del progetto" + "failed_update_project": "Errore durante l'aggiornamento dei server MCP del progetto", + "invalidJsonArgument": "Roo ha tentato di usare {{toolName}} con un argomento JSON non valido. Riprovo..." }, "info": { "server_restarting": "Riavvio del server MCP {{serverName}}...", diff --git a/src/i18n/locales/ja/mcp.json b/src/i18n/locales/ja/mcp.json index 2c35fe811b..c300ff861a 100644 --- a/src/i18n/locales/ja/mcp.json +++ b/src/i18n/locales/ja/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "MCP設定のJSONフォーマットが無効です。設定が正しいJSONフォーマットに従っていることを確認してください。", "invalid_settings_syntax": "MCP設定のJSONフォーマットが無効です。設定ファイルの構文エラーを確認してください。", "invalid_settings_validation": "MCP設定フォーマットが無効です:{{errorMessages}}", - "create_json": ".roo/mcp.jsonの作成または開くことに失敗しました:{{error}}", - "failed_update_project": "プロジェクトMCPサーバーの更新に失敗しました" + "failed_update_project": "プロジェクトMCPサーバーの更新に失敗しました", + "invalidJsonArgument": "Rooが無効なJSON引数で{{toolName}}を使用しようとしました。再試行中..." }, "info": { "server_restarting": "MCPサーバー{{serverName}}を再起動中...", diff --git a/src/i18n/locales/ko/mcp.json b/src/i18n/locales/ko/mcp.json index 2bf867fbd3..f5d8d7d03b 100644 --- a/src/i18n/locales/ko/mcp.json +++ b/src/i18n/locales/ko/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "잘못된 MCP 설정 JSON 형식입니다. 설정이 올바른 JSON 형식을 따르는지 확인하세요.", "invalid_settings_syntax": "잘못된 MCP 설정 JSON 형식입니다. 설정 파일의 구문 오류를 확인하세요.", "invalid_settings_validation": "잘못된 MCP 설정 형식: {{errorMessages}}", - "create_json": ".roo/mcp.json 생성 또는 열기 실패: {{error}}", - "failed_update_project": "프로젝트 MCP 서버 업데이트에 실패했습니다" + "failed_update_project": "프로젝트 MCP 서버 업데이트에 실패했습니다", + "invalidJsonArgument": "Roo가 유효하지 않은 JSON 인자로 {{toolName}}을(를) 사용하려고 했습니다. 다시 시도 중..." }, "info": { "server_restarting": "{{serverName}} MCP 서버를 재시작하는 중...", diff --git a/src/i18n/locales/nl/mcp.json b/src/i18n/locales/nl/mcp.json index 75abdf0a39..441ec2a9e5 100644 --- a/src/i18n/locales/nl/mcp.json +++ b/src/i18n/locales/nl/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Ongeldig MCP-instellingen JSON-formaat. Zorg ervoor dat je instellingen het juiste JSON-formaat volgen.", "invalid_settings_syntax": "Ongeldig MCP-instellingen JSON-formaat. Controleer je instellingenbestand op syntaxfouten.", "invalid_settings_validation": "Ongeldig MCP-instellingenformaat: {{errorMessages}}", - "create_json": "Aanmaken of openen van .roo/mcp.json mislukt: {{error}}", - "failed_update_project": "Bijwerken van project MCP-servers mislukt" + "failed_update_project": "Bijwerken van project MCP-servers mislukt", + "invalidJsonArgument": "Roo probeerde {{toolName}} te gebruiken met een ongeldig JSON-argument. Opnieuw proberen..." }, "info": { "server_restarting": "{{serverName}} MCP-server wordt opnieuw gestart...", diff --git a/src/i18n/locales/pl/mcp.json b/src/i18n/locales/pl/mcp.json index 7fbf08bef8..e6d49104b1 100644 --- a/src/i18n/locales/pl/mcp.json +++ b/src/i18n/locales/pl/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Nieprawidłowy format JSON ustawień MCP. Upewnij się, że Twoje ustawienia są zgodne z poprawnym formatem JSON.", "invalid_settings_syntax": "Nieprawidłowy format JSON ustawień MCP. Sprawdź, czy w pliku ustawień nie ma błędów składniowych.", "invalid_settings_validation": "Nieprawidłowy format ustawień MCP: {{errorMessages}}", - "create_json": "Nie udało się utworzyć lub otworzyć .roo/mcp.json: {{error}}", - "failed_update_project": "Nie udało się zaktualizować serwerów MCP projektu" + "failed_update_project": "Nie udało się zaktualizować serwerów MCP projektu", + "invalidJsonArgument": "Roo próbował użyć {{toolName}} z nieprawidłowym argumentem JSON. Ponawianie..." }, "info": { "server_restarting": "Ponowne uruchamianie serwera MCP {{serverName}}...", diff --git a/src/i18n/locales/pt-BR/mcp.json b/src/i18n/locales/pt-BR/mcp.json index 78b99056f3..4d1050e9db 100644 --- a/src/i18n/locales/pt-BR/mcp.json +++ b/src/i18n/locales/pt-BR/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Formato JSON das configurações MCP inválido. Por favor, verifique se suas configurações seguem o formato JSON correto.", "invalid_settings_syntax": "Formato JSON das configurações MCP inválido. Por favor, verifique se há erros de sintaxe no seu arquivo de configurações.", "invalid_settings_validation": "Formato de configurações MCP inválido: {{errorMessages}}", - "create_json": "Falha ao criar ou abrir .roo/mcp.json: {{error}}", - "failed_update_project": "Falha ao atualizar os servidores MCP do projeto" + "failed_update_project": "Falha ao atualizar os servidores MCP do projeto", + "invalidJsonArgument": "Roo tentou usar {{toolName}} com um argumento JSON inválido. Tentando novamente..." }, "info": { "server_restarting": "Reiniciando o servidor MCP {{serverName}}...", diff --git a/src/i18n/locales/ru/mcp.json b/src/i18n/locales/ru/mcp.json index 24c1cffd8f..fcbf6501f4 100644 --- a/src/i18n/locales/ru/mcp.json +++ b/src/i18n/locales/ru/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Неверный формат JSON настроек MCP. Пожалуйста, убедитесь, что ваши настройки соответствуют правильному формату JSON.", "invalid_settings_syntax": "Неверный формат JSON настроек MCP. Пожалуйста, проверьте ваш файл настроек на наличие синтаксических ошибок.", "invalid_settings_validation": "Неверный формат настроек MCP: {{errorMessages}}", - "create_json": "Не удалось создать или открыть .roo/mcp.json: {{error}}", - "failed_update_project": "Не удалось обновить серверы проекта MCP" + "failed_update_project": "Не удалось обновить серверы проекта MCP", + "invalidJsonArgument": "Roo попытался использовать {{toolName}} с недопустимым JSON-аргументом. Повторная попытка..." }, "info": { "server_restarting": "Перезапуск сервера MCP {{serverName}}...", diff --git a/src/i18n/locales/tr/mcp.json b/src/i18n/locales/tr/mcp.json index 841c962e77..bb5461e540 100644 --- a/src/i18n/locales/tr/mcp.json +++ b/src/i18n/locales/tr/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Geçersiz MCP ayarları JSON formatı. Lütfen ayarlarınızın doğru JSON formatını takip ettiğinden emin olun.", "invalid_settings_syntax": "Geçersiz MCP ayarları JSON formatı. Lütfen ayarlar dosyanızda sözdizimi hatalarını kontrol edin.", "invalid_settings_validation": "Geçersiz MCP ayarları formatı: {{errorMessages}}", - "create_json": ".roo/mcp.json oluşturulamadı veya açılamadı: {{error}}", - "failed_update_project": "Proje MCP sunucuları güncellenemedi" + "failed_update_project": "Proje MCP sunucuları güncellenemedi", + "invalidJsonArgument": "Roo, {{toolName}} aracını geçersiz bir JSON argümanıyla kullanmaya çalıştı. Tekrar deneniyor..." }, "info": { "server_restarting": "{{serverName}} MCP sunucusu yeniden başlatılıyor...", diff --git a/src/i18n/locales/vi/mcp.json b/src/i18n/locales/vi/mcp.json index 078f631b32..d0728ae0cb 100644 --- a/src/i18n/locales/vi/mcp.json +++ b/src/i18n/locales/vi/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "Định dạng JSON của cài đặt MCP không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng JSON chính xác.", "invalid_settings_syntax": "Định dạng JSON của cài đặt MCP không hợp lệ. Vui lòng kiểm tra lỗi cú pháp trong tệp cài đặt của bạn.", "invalid_settings_validation": "Định dạng cài đặt MCP không hợp lệ: {{errorMessages}}", - "create_json": "Không thể tạo hoặc mở .roo/mcp.json: {{error}}", - "failed_update_project": "Không thể cập nhật máy chủ MCP của dự án" + "failed_update_project": "Không thể cập nhật máy chủ MCP của dự án", + "invalidJsonArgument": "Roo đã cố gắng sử dụng {{toolName}} với tham số JSON không hợp lệ. Đang thử lại..." }, "info": { "server_restarting": "Đang khởi động lại máy chủ MCP {{serverName}}...", diff --git a/src/i18n/locales/zh-CN/mcp.json b/src/i18n/locales/zh-CN/mcp.json index 82c3451dd7..bb4abe6daf 100644 --- a/src/i18n/locales/zh-CN/mcp.json +++ b/src/i18n/locales/zh-CN/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "MCP设置JSON格式无效。请确保您的设置遵循正确的JSON格式。", "invalid_settings_syntax": "MCP设置JSON格式无效。请检查您的设置文件是否有语法错误。", "invalid_settings_validation": "MCP设置格式无效:{{errorMessages}}", - "create_json": "创建或打开 .roo/mcp.json 失败:{{error}}", - "failed_update_project": "更新项目MCP服务器失败" + "failed_update_project": "更新项目MCP服务器失败", + "invalidJsonArgument": "Roo 尝试使用无效的 JSON 参数调用 {{toolName}}。正在重试..." }, "info": { "server_restarting": "正在重启{{serverName}}MCP服务器...", diff --git a/src/i18n/locales/zh-TW/mcp.json b/src/i18n/locales/zh-TW/mcp.json index 84991dfde5..759ac93671 100644 --- a/src/i18n/locales/zh-TW/mcp.json +++ b/src/i18n/locales/zh-TW/mcp.json @@ -3,9 +3,9 @@ "invalid_settings_format": "MCP 設定 JSON 格式無效。請確保您的設定遵循正確的 JSON 格式。", "invalid_settings_syntax": "MCP 設定 JSON 格式無效。請檢查您的設定檔案是否有語法錯誤。", "invalid_settings_validation": "MCP 設定格式無效:{{errorMessages}}", - "create_json": "建立或開啟 .roo/mcp.json 失敗:{{error}}", - "failed_update_project": "更新專案 MCP 伺服器失敗" + "failed_update_project": "更新專案 MCP 伺服器失敗", + "invalidJsonArgument": "Roo 嘗試使用無效的 JSON 參數呼叫 {{toolName}}。正在重試..." }, "info": { "server_restarting": "正在重啟{{serverName}}MCP 伺服器...", diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index d95986539a..f2d5434f35 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState, memo } from "react" import { Server, ChevronDown } from "lucide-react" import { useEvent } from "react-use" +import { useTranslation } from "react-i18next" import { McpExecutionStatus, mcpExecutionStatusSchema } from "@roo-code/types" import { ExtensionMessage, ClineAskUseMcpServer } from "../../../../src/shared/ExtensionMessage" @@ -39,6 +40,8 @@ export const McpExecution = ({ useMcpServer, alwaysAllowMcp = false, }: McpExecutionProps) => { + const { t } = useTranslation("mcp") + // State for tracking MCP response status const [status, setStatus] = useState(null) const [responseText, setResponseText] = useState(text || "") @@ -182,10 +185,10 @@ export const McpExecution = ({ "text-vscode-errorForeground": status.status === "error", })}> {status.status === "started" - ? "Running" + ? t("execution.running") : status.status === "completed" - ? "Completed" - : "Error"} + ? t("execution.completed") + : t("execution.error")}
{status.status === "error" && "error" in status && status.error && (
({status.error})
diff --git a/webview-ui/src/i18n/locales/ca/mcp.json b/webview-ui/src/i18n/locales/ca/mcp.json index 4263ac763d..ab3173cfad 100644 --- a/webview-ui/src/i18n/locales/ca/mcp.json +++ b/webview-ui/src/i18n/locales/ca/mcp.json @@ -54,5 +54,10 @@ "retrying": "Tornant a intentar...", "retryConnection": "Torna a intentar la connexió" }, - "refreshMCP": "Actualitza els servidors MCP" + "refreshMCP": "Actualitza els servidors MCP", + "execution": { + "running": "En execució", + "completed": "Completat", + "error": "Error" + } } diff --git a/webview-ui/src/i18n/locales/de/mcp.json b/webview-ui/src/i18n/locales/de/mcp.json index c382be3e90..3fbc9ea1de 100644 --- a/webview-ui/src/i18n/locales/de/mcp.json +++ b/webview-ui/src/i18n/locales/de/mcp.json @@ -54,5 +54,10 @@ "retrying": "Wiederhole...", "retryConnection": "Verbindung wiederholen" }, - "refreshMCP": "MCP-Server aktualisieren" + "refreshMCP": "MCP-Server aktualisieren", + "execution": { + "running": "Wird ausgeführt", + "completed": "Abgeschlossen", + "error": "Fehler" + } } diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 6136ed9b1d..9c2c8437da 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -54,5 +54,10 @@ "serverStatus": { "retrying": "Retrying...", "retryConnection": "Retry Connection" + }, + "execution": { + "running": "Running", + "completed": "Completed", + "error": "Error" } } diff --git a/webview-ui/src/i18n/locales/es/mcp.json b/webview-ui/src/i18n/locales/es/mcp.json index a5e1d3d562..27af50ddc0 100644 --- a/webview-ui/src/i18n/locales/es/mcp.json +++ b/webview-ui/src/i18n/locales/es/mcp.json @@ -54,5 +54,10 @@ "retrying": "Reintentando...", "retryConnection": "Reintentar conexión" }, - "refreshMCP": "Actualizar Servidores MCP" + "refreshMCP": "Actualizar Servidores MCP", + "execution": { + "running": "Ejecutando", + "completed": "Completado", + "error": "Error" + } } diff --git a/webview-ui/src/i18n/locales/fr/mcp.json b/webview-ui/src/i18n/locales/fr/mcp.json index af80f6f680..1cde3bad23 100644 --- a/webview-ui/src/i18n/locales/fr/mcp.json +++ b/webview-ui/src/i18n/locales/fr/mcp.json @@ -54,5 +54,10 @@ "retrying": "Nouvelle tentative...", "retryConnection": "Réessayer la connexion" }, - "refreshMCP": "Rafraîchir les serveurs MCP" + "refreshMCP": "Rafraîchir les serveurs MCP", + "execution": { + "running": "En cours", + "completed": "Terminé", + "error": "Erreur" + } } diff --git a/webview-ui/src/i18n/locales/hi/mcp.json b/webview-ui/src/i18n/locales/hi/mcp.json index 0a2465ee34..d0339a2bde 100644 --- a/webview-ui/src/i18n/locales/hi/mcp.json +++ b/webview-ui/src/i18n/locales/hi/mcp.json @@ -54,5 +54,10 @@ "retrying": "फिर से कोशिश कर रहा है...", "retryConnection": "कनेक्शन फिर से आज़माएँ" }, - "refreshMCP": "एमसीपी सर्वर रीफ्रेश करें" + "refreshMCP": "एमसीपी सर्वर रीफ्रेश करें", + "execution": { + "running": "चल रहा है", + "completed": "पूरा हुआ", + "error": "त्रुटि" + } } diff --git a/webview-ui/src/i18n/locales/it/mcp.json b/webview-ui/src/i18n/locales/it/mcp.json index 93bcf30db8..e60c1c1ef9 100644 --- a/webview-ui/src/i18n/locales/it/mcp.json +++ b/webview-ui/src/i18n/locales/it/mcp.json @@ -54,5 +54,10 @@ "retrying": "Riprovo...", "retryConnection": "Riprova connessione" }, - "refreshMCP": "Aggiorna server MCP" + "refreshMCP": "Aggiorna server MCP", + "execution": { + "running": "In esecuzione", + "completed": "Completato", + "error": "Errore" + } } diff --git a/webview-ui/src/i18n/locales/ja/mcp.json b/webview-ui/src/i18n/locales/ja/mcp.json index c64d54b3a6..204bdca1e4 100644 --- a/webview-ui/src/i18n/locales/ja/mcp.json +++ b/webview-ui/src/i18n/locales/ja/mcp.json @@ -54,5 +54,10 @@ "retrying": "再試行中...", "retryConnection": "再接続" }, - "refreshMCP": "MCPサーバーを更新" + "refreshMCP": "MCPサーバーを更新", + "execution": { + "running": "実行中", + "completed": "完了", + "error": "エラー" + } } diff --git a/webview-ui/src/i18n/locales/ko/mcp.json b/webview-ui/src/i18n/locales/ko/mcp.json index b421bf97ea..957f4ee69a 100644 --- a/webview-ui/src/i18n/locales/ko/mcp.json +++ b/webview-ui/src/i18n/locales/ko/mcp.json @@ -54,5 +54,10 @@ "retrying": "다시 시도 중...", "retryConnection": "연결 다시 시도" }, - "refreshMCP": "MCP 서버 새로 고침" + "refreshMCP": "MCP 서버 새로 고침", + "execution": { + "running": "실행 중", + "completed": "완료됨", + "error": "오류" + } } diff --git a/webview-ui/src/i18n/locales/nl/mcp.json b/webview-ui/src/i18n/locales/nl/mcp.json index 6a37ed9fc4..571dc36e9e 100644 --- a/webview-ui/src/i18n/locales/nl/mcp.json +++ b/webview-ui/src/i18n/locales/nl/mcp.json @@ -54,5 +54,10 @@ "retrying": "Opnieuw proberen...", "retryConnection": "Verbinding opnieuw proberen" }, - "refreshMCP": "MCP-servers vernieuwen" + "refreshMCP": "MCP-servers vernieuwen", + "execution": { + "running": "Wordt uitgevoerd", + "completed": "Voltooid", + "error": "Fout" + } } diff --git a/webview-ui/src/i18n/locales/pl/mcp.json b/webview-ui/src/i18n/locales/pl/mcp.json index 70333d3d3a..4a694aac4d 100644 --- a/webview-ui/src/i18n/locales/pl/mcp.json +++ b/webview-ui/src/i18n/locales/pl/mcp.json @@ -54,5 +54,10 @@ "retrying": "Ponawianie...", "retryConnection": "Ponów połączenie" }, - "refreshMCP": "Odśwież serwery MCP" + "refreshMCP": "Odśwież serwery MCP", + "execution": { + "running": "Uruchomione", + "completed": "Zakończone", + "error": "Błąd" + } } diff --git a/webview-ui/src/i18n/locales/pt-BR/mcp.json b/webview-ui/src/i18n/locales/pt-BR/mcp.json index 10bd55c793..7b1cbbd19d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/mcp.json +++ b/webview-ui/src/i18n/locales/pt-BR/mcp.json @@ -54,5 +54,10 @@ "retrying": "Tentando novamente...", "retryConnection": "Tentar reconectar" }, - "refreshMCP": "Atualizar Servidores MCP" + "refreshMCP": "Atualizar Servidores MCP", + "execution": { + "running": "Em execução", + "completed": "Concluído", + "error": "Erro" + } } diff --git a/webview-ui/src/i18n/locales/ru/mcp.json b/webview-ui/src/i18n/locales/ru/mcp.json index 03d1e70703..8f064d168c 100644 --- a/webview-ui/src/i18n/locales/ru/mcp.json +++ b/webview-ui/src/i18n/locales/ru/mcp.json @@ -54,5 +54,10 @@ "retrying": "Повторная попытка...", "retryConnection": "Повторить подключение" }, - "refreshMCP": "Обновить MCP серверы" + "refreshMCP": "Обновить MCP серверы", + "execution": { + "running": "Выполняется", + "completed": "Завершено", + "error": "Ошибка" + } } diff --git a/webview-ui/src/i18n/locales/tr/mcp.json b/webview-ui/src/i18n/locales/tr/mcp.json index 2a74873cfe..e8b6b5b4fe 100644 --- a/webview-ui/src/i18n/locales/tr/mcp.json +++ b/webview-ui/src/i18n/locales/tr/mcp.json @@ -54,5 +54,10 @@ "retrying": "Tekrar deneniyor...", "retryConnection": "Bağlantıyı tekrar dene" }, - "refreshMCP": "MCP Sunucularını Yenile" + "refreshMCP": "MCP Sunucularını Yenile", + "execution": { + "running": "Çalışıyor", + "completed": "Tamamlandı", + "error": "Hata" + } } diff --git a/webview-ui/src/i18n/locales/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index 1bb972d7bb..11c18003a2 100644 --- a/webview-ui/src/i18n/locales/vi/mcp.json +++ b/webview-ui/src/i18n/locales/vi/mcp.json @@ -54,5 +54,10 @@ "retrying": "Đang thử lại...", "retryConnection": "Thử kết nối lại" }, - "refreshMCP": "Làm mới Máy chủ MCP" + "refreshMCP": "Làm mới Máy chủ MCP", + "execution": { + "running": "Đang chạy", + "completed": "Hoàn thành", + "error": "Lỗi" + } } diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index c9cdfd5af7..67d47ea75e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/mcp.json +++ b/webview-ui/src/i18n/locales/zh-CN/mcp.json @@ -54,5 +54,10 @@ "retrying": "重试中...", "retryConnection": "重试连接" }, - "refreshMCP": "刷新 MCP 服务器" + "refreshMCP": "刷新 MCP 服务器", + "execution": { + "running": "运行中", + "completed": "已完成", + "error": "错误" + } } diff --git a/webview-ui/src/i18n/locales/zh-TW/mcp.json b/webview-ui/src/i18n/locales/zh-TW/mcp.json index 13123fc6a5..9406433ae9 100644 --- a/webview-ui/src/i18n/locales/zh-TW/mcp.json +++ b/webview-ui/src/i18n/locales/zh-TW/mcp.json @@ -54,5 +54,10 @@ "retrying": "重試中...", "retryConnection": "重試連線" }, - "refreshMCP": "重新整理 MCP 伺服器" + "refreshMCP": "重新整理 MCP 伺服器", + "execution": { + "running": "執行中", + "completed": "已完成", + "error": "錯誤" + } } From 3a434bed3abe63b83a476ffe663f53b61195ba00 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 12 Jun 2025 10:06:51 +0700 Subject: [PATCH 7/8] test: add parameter validation tests for missing server_name and tool_name --- src/core/tools/__tests__/useMcpToolTool.test.ts | 9 +++++++++ webview-ui/src/components/chat/McpExecution.tsx | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/tools/__tests__/useMcpToolTool.test.ts b/src/core/tools/__tests__/useMcpToolTool.test.ts index 4c4ae5fbef..24fa2540c0 100644 --- a/src/core/tools/__tests__/useMcpToolTool.test.ts +++ b/src/core/tools/__tests__/useMcpToolTool.test.ts @@ -12,6 +12,15 @@ jest.mock("../../prompts/responses", () => ({ }, })) +jest.mock("../../../i18n", () => ({ + t: jest.fn((key: string, params?: any) => { + if (key === "mcp:errors.invalidJsonArgument" && params?.toolName) { + return `Roo tried to use ${params.toolName} with an invalid JSON argument. Retrying...` + } + return key + }), +})) + describe("useMcpToolTool", () => { let mockTask: Partial let mockAskApproval: jest.Mock diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index f2d5434f35..93410a1bc4 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -10,7 +10,7 @@ import { cn } from "@src/lib/utils" import { Button } from "@src/components/ui" import CodeBlock from "../common/CodeBlock" import McpToolRow from "../mcp/McpToolRow" -import { Markdown } from "../ui/markdown" +import { Markdown } from "./Markdown" interface McpExecutionProps { executionId: string @@ -286,7 +286,7 @@ const ResponseContainerInternal = ({ "max-h-[100%] mt-1 pt-1": isExpanded && !hasArguments, })}> {response.length > 0 && - (isJson ? : )} + (isJson ? : )}
) From e10b6843e1f57000735ed12964e2a0945ef2e31d Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Wed, 11 Jun 2025 23:37:37 -0500 Subject: [PATCH 8/8] Update McpExecution component to show server name in header and tool name in approval section --- webview-ui/src/components/chat/McpExecution.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index 93410a1bc4..3b7f6be4ee 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -160,12 +160,10 @@ export const McpExecution = ({ return ( <>
-
+
-
+
{serverName && {serverName}} - {serverName && toolName && } - {toolName && {toolName}}