mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-10 22:41:14 +00:00
feat(mcp): improve mcp with McpExecution, make UX like terminal execute (#3485)
* 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 * refactor: merge loops in combineCommandSequences for better efficiency - 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 * feat: enhance JSON parsing and rendering in McpExecution component * feat: add mock implementations for react-markdown and remark-gfm * fix: remove unreachable return statement in ChatRowContent component * feat(i18n): add execution status messages and error handling for invalid JSON arguments * test: add parameter validation tests for missing server_name and tool_name * Update McpExecution component to show server name in header and tool name in approval section --------- Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
parent
1b1e5a20f1
commit
a9c1dad2ce
47 changed files with 1249 additions and 275 deletions
|
|
@ -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"
|
||||
|
|
|
|||
31
packages/types/src/mcp.ts
Normal file
31
packages/types/src/mcp.ts
Normal file
|
|
@ -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<typeof mcpExecutionStatusSchema>
|
||||
269
src/core/tools/__tests__/useMcpToolTool.test.ts
Normal file
269
src/core/tools/__tests__/useMcpToolTool.test.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
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}`),
|
||||
},
|
||||
}))
|
||||
|
||||
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<Task>
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -2,6 +2,167 @@ 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"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
interface McpToolParams {
|
||||
server_name?: string
|
||||
tool_name?: string
|
||||
arguments?: string
|
||||
}
|
||||
|
||||
type ValidationResult =
|
||||
| { isValid: false }
|
||||
| {
|
||||
isValid: true
|
||||
serverName: string
|
||||
toolName: string
|
||||
parsedArguments?: Record<string, unknown>
|
||||
}
|
||||
|
||||
async function handlePartialRequest(
|
||||
cline: Task,
|
||||
params: McpToolParams,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
): Promise<void> {
|
||||
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<ValidationResult> {
|
||||
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<string, unknown> | undefined
|
||||
|
||||
if (params.arguments) {
|
||||
try {
|
||||
parsedArguments = JSON.parse(params.arguments)
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("use_mcp_tool")
|
||||
await cline.say("error", t("mcp:errors.invalidJsonArgument", { toolName: params.tool_name }))
|
||||
|
||||
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<void> {
|
||||
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<string, unknown> | undefined,
|
||||
executionId: string,
|
||||
pushToolResult: PushToolResult,
|
||||
): Promise<void> {
|
||||
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 +172,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(() => {})
|
||||
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<string, unknown> | 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
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
type: "use_mcp_tool",
|
||||
serverName: server_name,
|
||||
toolName: tool_name,
|
||||
arguments: mcp_arguments,
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
||||
await handlePartialRequest(cline, params, removeClosingTag)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate parameters
|
||||
const validation = await validateParams(cline, params, pushToolResult)
|
||||
if (!validation.isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
const { serverName, toolName, parsedArguments } = validation
|
||||
|
||||
// Reset mistake count on successful validation
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Get user approval
|
||||
const completeMessage = JSON.stringify({
|
||||
type: "use_mcp_tool",
|
||||
serverName,
|
||||
toolName,
|
||||
arguments: params.arguments,
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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 सर्वर पुनः प्रारंभ हो रहा है...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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}}を再起動中...",
|
||||
|
|
|
|||
|
|
@ -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 서버를 재시작하는 중...",
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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}}...",
|
||||
|
|
|
|||
|
|
@ -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服务器...",
|
||||
|
|
|
|||
|
|
@ -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 伺服器...",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,183 @@
|
|||
// npx jest src/shared/__tests__/combineCommandSequences.test.ts
|
||||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { combineCommandSequences } from "../combineCommandSequences"
|
||||
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1745710928469,
|
||||
type: "say",
|
||||
say: "api_req_started",
|
||||
text: '{"request":"<task>\\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",
|
||||
},
|
||||
]
|
||||
import { ClineMessage } from "@roo-code/types"
|
||||
|
||||
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("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 },
|
||||
]
|
||||
|
||||
const result = combineCommandSequences(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
type: "ask",
|
||||
ask: "command",
|
||||
text: "ls\nOutput:file1.txt\nfile2.txt",
|
||||
ts: 1625097600000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
|
|
@ -23,14 +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 combinedMessages = new Map<number, ClineMessage>()
|
||||
const processedIndices = new Set<number>()
|
||||
|
||||
// First pass: combine commands with their outputs.
|
||||
// Single pass through all messages
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (messages[i].type === "ask" && messages[i].ask === "command") {
|
||||
let combinedText = messages[i].text || ""
|
||||
const msg = messages[i]
|
||||
|
||||
// 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++
|
||||
}
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
// Parse the JSON from the message text
|
||||
const jsonObj = safeJsonParse<any>(msg.text || "{}", {})
|
||||
|
||||
// Add the response to the JSON object
|
||||
jsonObj.response = responses.join("\n")
|
||||
|
||||
// Stringify the updated JSON object
|
||||
const combinedText = JSON.stringify(jsonObj)
|
||||
|
||||
combinedMessages.set(msg.ts, { ...msg, text: combinedText })
|
||||
} else {
|
||||
// If there's no response, just keep the original message
|
||||
combinedMessages.set(msg.ts, { ...msg })
|
||||
}
|
||||
}
|
||||
// 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]
|
||||
|
|
@ -47,33 +90,56 @@ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(`[combineCommandSequences] combinedCommands ->`, messages, combinedCommands)
|
||||
// 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]
|
||||
|
||||
// Second pass: remove command_outputs and replace original commands with
|
||||
// combined ones.
|
||||
return messages
|
||||
.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
|
||||
.map((msg) => {
|
||||
if (msg.type === "ask" && msg.ask === "command") {
|
||||
return combinedCommands.find((cmd) => cmd.ts === msg.ts) || msg
|
||||
}
|
||||
// Skip messages that were processed as outputs/responses
|
||||
if (processedIndices.has(i)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return msg
|
||||
})
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ module.exports = {
|
|||
"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
|
||||
"^@src/utils/highlighter$": "<rootDir>/src/__mocks__/utils/highlighter.ts",
|
||||
"^shiki$": "<rootDir>/src/__mocks__/shiki.ts",
|
||||
"^react-markdown$": "<rootDir>/src/__mocks__/react-markdown.tsx",
|
||||
"^remark-gfm$": "<rootDir>/src/__mocks__/remark-gfm.ts",
|
||||
},
|
||||
reporters: [["jest-simple-dot-reporter", {}]],
|
||||
transformIgnorePatterns: [
|
||||
|
|
|
|||
19
webview-ui/src/__mocks__/react-markdown.tsx
Normal file
19
webview-ui/src/__mocks__/react-markdown.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react"
|
||||
|
||||
interface ReactMarkdownProps {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
remarkPlugins?: any[]
|
||||
components?: any
|
||||
}
|
||||
|
||||
const ReactMarkdown: React.FC<ReactMarkdownProps> = ({ children, className }) => {
|
||||
return (
|
||||
<div className={className} data-testid="mock-react-markdown">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReactMarkdown
|
||||
export type { ReactMarkdownProps as Options }
|
||||
3
webview-ui/src/__mocks__/remark-gfm.ts
Normal file
3
webview-ui/src/__mocks__/remark-gfm.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const remarkGfm = () => {}
|
||||
|
||||
export default remarkGfm
|
||||
|
|
@ -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,6 @@ export const ChatRowContent = ({
|
|||
)
|
||||
case "shell_integration_warning":
|
||||
return <CommandExecutionError />
|
||||
case "mcp_server_response":
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingTop: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{t("chat:response")}
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={message.text}
|
||||
language="json"
|
||||
isExpanded={true}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "checkpoint_saved":
|
||||
return (
|
||||
<CheckpointSaved
|
||||
|
|
@ -1070,7 +1048,17 @@ export const ChatRowContent = ({
|
|||
/>
|
||||
)
|
||||
case "use_mcp_server":
|
||||
const useMcpServer = safeJsonParse<ClineAskUseMcpServer>(message.text)
|
||||
// Parse the message text to get the MCP server request
|
||||
const messageJson = safeJsonParse<any>(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 +1072,7 @@ export const ChatRowContent = ({
|
|||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
borderRadius: "3px",
|
||||
padding: "8px 10px",
|
||||
marginTop: "8px",
|
||||
}}>
|
||||
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2 mt-2">
|
||||
{useMcpServer.type === "access_mcp_resource" && (
|
||||
<McpResourceRow
|
||||
item={{
|
||||
|
|
@ -1110,45 +1092,16 @@ export const ChatRowContent = ({
|
|||
/>
|
||||
)}
|
||||
{useMcpServer.type === "use_mcp_tool" && (
|
||||
<>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find(
|
||||
(tool) => tool.name === useMcpServer.toolName,
|
||||
)?.description || "",
|
||||
alwaysAllow:
|
||||
server?.tools?.find(
|
||||
(tool) => tool.name === useMcpServer.toolName,
|
||||
)?.alwaysAllow || false,
|
||||
}}
|
||||
serverName={useMcpServer.serverName}
|
||||
serverSource={server?.source}
|
||||
alwaysAllowMcp={alwaysAllowMcp}
|
||||
/>
|
||||
</div>
|
||||
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{t("chat:arguments")}
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={useMcpServer.arguments}
|
||||
language="json"
|
||||
isExpanded={true}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<McpExecution
|
||||
executionId={message.ts.toString()}
|
||||
text={useMcpServer.arguments !== "{}" ? useMcpServer.arguments : undefined}
|
||||
serverName={useMcpServer.serverName}
|
||||
toolName={useMcpServer.toolName}
|
||||
isArguments={true}
|
||||
server={server}
|
||||
useMcpServer={useMcpServer}
|
||||
alwaysAllowMcp={alwaysAllowMcp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
291
webview-ui/src/components/chat/McpExecution.tsx
Normal file
291
webview-ui/src/components/chat/McpExecution.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
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"
|
||||
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"
|
||||
import { Markdown } from "./Markdown"
|
||||
|
||||
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) => {
|
||||
const { t } = useTranslation("mcp")
|
||||
|
||||
// State for tracking MCP response status
|
||||
const [status, setStatus] = useState<McpExecutionStatus | null>(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 JSON and return both the result and formatted text
|
||||
const tryParseJson = (text: string): { isJson: boolean; formatted: string } => {
|
||||
if (!text) return { isJson: false, formatted: "" }
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
return {
|
||||
isJson: true,
|
||||
formatted: JSON.stringify(parsed, null, 2),
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
isJson: false,
|
||||
formatted: text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}, [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<any>(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 (
|
||||
<>
|
||||
<div className="flex flex-row items-center justify-between gap-2 mb-1">
|
||||
<div className="flex flex-row items-center gap-1 flex-wrap">
|
||||
<Server size={16} className="text-vscode-descriptionForeground" />
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{serverName && <span className="font-bold text-vscode-foreground">{serverName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between gap-2 px-1">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{status && (
|
||||
<div className="flex flex-row items-center gap-2 font-mono text-xs">
|
||||
<div
|
||||
className={cn("rounded-full size-1.5", {
|
||||
"bg-lime-400": status.status === "started" || status.status === "completed",
|
||||
"bg-red-400": status.status === "error",
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("whitespace-nowrap", {
|
||||
"text-vscode-foreground":
|
||||
status.status === "started" || status.status === "completed",
|
||||
"text-vscode-errorForeground": status.status === "error",
|
||||
})}>
|
||||
{status.status === "started"
|
||||
? t("execution.running")
|
||||
: status.status === "completed"
|
||||
? t("execution.completed")
|
||||
: t("execution.error")}
|
||||
</div>
|
||||
{status.status === "error" && "error" in status && status.error && (
|
||||
<div className="whitespace-nowrap">({status.error})</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{responseText && responseText.length > 0 && (
|
||||
<Button variant="ghost" size="icon" onClick={onToggleResponseExpand}>
|
||||
<ChevronDown
|
||||
className={cn("size-4 transition-transform duration-300", {
|
||||
"rotate-180": isResponseExpanded,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-vscode-editor-background rounded-xs p-2">
|
||||
{/* Tool information section */}
|
||||
{useMcpServer?.type === "use_mcp_tool" && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.description ||
|
||||
"",
|
||||
alwaysAllow:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.alwaysAllow ||
|
||||
false,
|
||||
}}
|
||||
serverName={useMcpServer.serverName}
|
||||
serverSource={server?.source}
|
||||
alwaysAllowMcp={alwaysAllowMcp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!useMcpServer && toolName && serverName && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: toolName || "",
|
||||
description: "",
|
||||
alwaysAllow: false,
|
||||
}}
|
||||
serverName={serverName}
|
||||
serverSource={undefined}
|
||||
alwaysAllowMcp={alwaysAllowMcp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Arguments section - display like command (always visible) */}
|
||||
{(isArguments || useMcpServer?.arguments || argumentsText) && (
|
||||
<div
|
||||
className={cn({
|
||||
"mt-1 pt-1":
|
||||
!isArguments && (useMcpServer?.type === "use_mcp_tool" || (toolName && serverName)),
|
||||
})}>
|
||||
<CodeBlock source={formattedArgumentsText} language="json" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Response section - collapsible like command output */}
|
||||
<ResponseContainer
|
||||
isExpanded={isResponseExpanded}
|
||||
response={formattedResponseText}
|
||||
isJson={responseIsJson}
|
||||
hasArguments={!!(isArguments || useMcpServer?.arguments || argumentsText)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
McpExecution.displayName = "McpExecution"
|
||||
|
||||
const ResponseContainerInternal = ({
|
||||
isExpanded,
|
||||
response,
|
||||
isJson,
|
||||
hasArguments,
|
||||
}: {
|
||||
isExpanded: boolean
|
||||
response: string
|
||||
isJson: boolean
|
||||
hasArguments?: boolean
|
||||
}) => (
|
||||
<div
|
||||
className={cn("overflow-hidden", {
|
||||
"max-h-0": !isExpanded,
|
||||
"max-h-[100%] mt-1 pt-1 border-t border-border/25": isExpanded && hasArguments,
|
||||
"max-h-[100%] mt-1 pt-1": isExpanded && !hasArguments,
|
||||
})}>
|
||||
{response.length > 0 &&
|
||||
(isJson ? <CodeBlock source={response} language="json" /> : <Markdown markdown={response} partial={false} />)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const ResponseContainer = memo(ResponseContainerInternal)
|
||||
|
|
@ -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 (
|
||||
<ToolUseBlock>
|
||||
{hasHeader && (
|
||||
<ToolUseBlockHeader onClick={onToggleExpand}>
|
||||
{isLoading && <VSCodeProgressRing className="size-3 mr-2" />}
|
||||
{isFeedback ? (
|
||||
{header ? (
|
||||
<div className="flex items-center">
|
||||
<span className="codicon codicon-server mr-1.5"></span>
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2">{header}</span>
|
||||
</div>
|
||||
) : isFeedback ? (
|
||||
<div className="flex items-center">
|
||||
<span className={`codicon codicon-${isFeedback ? "feedback" : "codicon-output"} mr-1.5`} />
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2 rtl">
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"serverStatus": {
|
||||
"retrying": "Retrying...",
|
||||
"retryConnection": "Retry Connection"
|
||||
},
|
||||
"execution": {
|
||||
"running": "Running",
|
||||
"completed": "Completed",
|
||||
"error": "Error"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "फिर से कोशिश कर रहा है...",
|
||||
"retryConnection": "कनेक्शन फिर से आज़माएँ"
|
||||
},
|
||||
"refreshMCP": "एमसीपी सर्वर रीफ्रेश करें"
|
||||
"refreshMCP": "एमसीपी सर्वर रीफ्रेश करें",
|
||||
"execution": {
|
||||
"running": "चल रहा है",
|
||||
"completed": "पूरा हुआ",
|
||||
"error": "त्रुटि"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "再試行中...",
|
||||
"retryConnection": "再接続"
|
||||
},
|
||||
"refreshMCP": "MCPサーバーを更新"
|
||||
"refreshMCP": "MCPサーバーを更新",
|
||||
"execution": {
|
||||
"running": "実行中",
|
||||
"completed": "完了",
|
||||
"error": "エラー"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "다시 시도 중...",
|
||||
"retryConnection": "연결 다시 시도"
|
||||
},
|
||||
"refreshMCP": "MCP 서버 새로 고침"
|
||||
"refreshMCP": "MCP 서버 새로 고침",
|
||||
"execution": {
|
||||
"running": "실행 중",
|
||||
"completed": "완료됨",
|
||||
"error": "오류"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "Повторная попытка...",
|
||||
"retryConnection": "Повторить подключение"
|
||||
},
|
||||
"refreshMCP": "Обновить MCP серверы"
|
||||
"refreshMCP": "Обновить MCP серверы",
|
||||
"execution": {
|
||||
"running": "Выполняется",
|
||||
"completed": "Завершено",
|
||||
"error": "Ошибка"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "重试中...",
|
||||
"retryConnection": "重试连接"
|
||||
},
|
||||
"refreshMCP": "刷新 MCP 服务器"
|
||||
"refreshMCP": "刷新 MCP 服务器",
|
||||
"execution": {
|
||||
"running": "运行中",
|
||||
"completed": "已完成",
|
||||
"error": "错误"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,10 @@
|
|||
"retrying": "重試中...",
|
||||
"retryConnection": "重試連線"
|
||||
},
|
||||
"refreshMCP": "重新整理 MCP 伺服器"
|
||||
"refreshMCP": "重新整理 MCP 伺服器",
|
||||
"execution": {
|
||||
"running": "執行中",
|
||||
"completed": "已完成",
|
||||
"error": "錯誤"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue