diff --git a/webview-ui/src/components/mcp/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx
index 58b938f9f1..aa57b18fd9 100644
--- a/webview-ui/src/components/mcp/McpToolRow.tsx
+++ b/webview-ui/src/components/mcp/McpToolRow.tsx
@@ -4,7 +4,7 @@ import { McpTool } from "@roo/mcp"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
-import { StandardTooltip } from "@/components/ui"
+import { StandardTooltip, ToggleSwitch } from "@/components/ui"
type McpToolRowProps = {
tool: McpTool
@@ -16,6 +16,8 @@ type McpToolRowProps = {
const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => {
const { t } = useAppTranslation()
+ const isToolEnabled = tool.enabledForPrompt ?? true
+
const handleAlwaysAllowChange = () => {
if (!serverName) return
vscode.postMessage({
@@ -46,17 +48,29 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
onClick={(e) => e.stopPropagation()}>
{/* Tool name section */}
-
+
- {tool.name}
+
+ {tool.name}
+
{/* Controls section */}
{serverName && (
- {/* Always Allow checkbox */}
- {alwaysAllowMcp && (
+ {/* Always Allow checkbox - only show when tool is enabled */}
+ {alwaysAllowMcp && isToolEnabled && (
)}
- {/* Enabled eye button - only show in settings context */}
+ {/* Enabled toggle switch - only show in settings context */}
{!isInChatContext && (
-
+ data-testid={`tool-prompt-toggle-${tool.name}`}
+ />
)}
)}
{tool.description && (
- {tool.description}
+
+ {tool.description}
+
)}
- {tool.inputSchema &&
+ {isToolEnabled &&
+ tool.inputSchema &&
"properties" in tool.inputSchema &&
Object.keys(tool.inputSchema.properties as Record).length > 0 && (
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index 2c83b432cc..b95ed2608c 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -22,6 +22,7 @@ import {
DialogTitle,
DialogDescription,
DialogFooter,
+ ToggleSwitch,
} from "@src/components/ui"
import { buildDocLink } from "@src/utils/docLinks"
@@ -295,54 +296,6 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
style={{ marginRight: "8px" }}>
-
{
- vscode.postMessage({
- type: "toggleMcpServer",
- serverName: server.name,
- source: server.source || "global",
- disabled: !server.disabled,
- })
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault()
- vscode.postMessage({
- type: "toggleMcpServer",
- serverName: server.name,
- source: server.source || "global",
- disabled: !server.disabled,
- })
- }
- }}>
-
-
+
+ {
+ vscode.postMessage({
+ type: "toggleMcpServer",
+ serverName: server.name,
+ source: server.source || "global",
+ disabled: !server.disabled,
+ })
+ }}
+ size="medium"
+ aria-label={`Toggle ${server.name} server`}
+ />
+
{server.status === "connected" ? (
diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
index 2bfe3b1338..f686a23f00 100644
--- a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
+++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
@@ -144,40 +144,40 @@ describe("McpToolRow", () => {
expect(screen.getByText("Second parameter")).toBeInTheDocument()
})
- it("shows eye button when serverName is provided and not in chat context", () => {
+ it("shows toggle switch when serverName is provided and not in chat context", () => {
render()
- const eyeButton = screen.getByRole("button", { name: "Toggle prompt inclusion" })
- expect(eyeButton).toBeInTheDocument()
+ const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
+ expect(toggleSwitch).toBeInTheDocument()
})
- it("hides eye button when isInChatContext is true", () => {
+ it("hides toggle switch when isInChatContext is true", () => {
render()
- const eyeButton = screen.queryByRole("button", { name: "Toggle prompt inclusion" })
- expect(eyeButton).not.toBeInTheDocument()
+ const toggleSwitch = screen.queryByRole("switch", { name: "Toggle prompt inclusion" })
+ expect(toggleSwitch).not.toBeInTheDocument()
})
- it("shows correct eye icon based on enabledForPrompt state", () => {
- // Test when enabled (should show eye-closed icon)
+ it("shows correct toggle switch state based on enabledForPrompt", () => {
+ // Test when enabled (should be checked)
const { rerender } = render()
- let eyeIcon = screen.getByRole("button", { name: "Toggle prompt inclusion" }).querySelector("span")
- expect(eyeIcon).toHaveClass("codicon-eye-closed")
+ let toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
+ expect(toggleSwitch).toHaveAttribute("aria-checked", "true")
- // Test when disabled (should show eye icon)
+ // Test when disabled (should not be checked)
const disabledTool = { ...mockTool, enabledForPrompt: false }
rerender()
- eyeIcon = screen.getByRole("button", { name: "Toggle prompt inclusion" }).querySelector("span")
- expect(eyeIcon).toHaveClass("codicon-eye")
+ toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
+ expect(toggleSwitch).toHaveAttribute("aria-checked", "false")
})
- it("sends message to toggle enabledForPrompt when eye button is clicked", () => {
+ it("sends message to toggle enabledForPrompt when toggle switch is clicked", () => {
render()
- const eyeButton = screen.getByRole("button", { name: "Toggle prompt inclusion" })
- fireEvent.click(eyeButton)
+ const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" })
+ fireEvent.click(toggleSwitch)
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "toggleToolEnabledForPrompt",
@@ -187,4 +187,102 @@ describe("McpToolRow", () => {
isEnabled: false,
})
})
+
+ it("hides always allow checkbox when tool is disabled", () => {
+ const disabledTool = { ...mockTool, enabledForPrompt: false }
+ render()
+
+ expect(screen.queryByText("Always allow")).not.toBeInTheDocument()
+ })
+
+ it("shows always allow checkbox when tool is enabled", () => {
+ const enabledTool = { ...mockTool, enabledForPrompt: true }
+ render()
+
+ expect(screen.getByText("Always allow")).toBeInTheDocument()
+ })
+
+ it("hides parameters section when tool is disabled", () => {
+ const disabledToolWithSchema = {
+ ...mockTool,
+ enabledForPrompt: false,
+ inputSchema: {
+ type: "object",
+ properties: {
+ param1: {
+ type: "string",
+ description: "First parameter",
+ },
+ },
+ required: ["param1"],
+ },
+ }
+
+ render()
+
+ expect(screen.queryByText("Parameters")).not.toBeInTheDocument()
+ expect(screen.queryByText("param1")).not.toBeInTheDocument()
+ expect(screen.queryByText("First parameter")).not.toBeInTheDocument()
+ })
+
+ it("shows parameters section when tool is enabled", () => {
+ const enabledToolWithSchema = {
+ ...mockTool,
+ enabledForPrompt: true,
+ inputSchema: {
+ type: "object",
+ properties: {
+ param1: {
+ type: "string",
+ description: "First parameter",
+ },
+ },
+ required: ["param1"],
+ },
+ }
+
+ render()
+
+ expect(screen.getByText("Parameters")).toBeInTheDocument()
+ expect(screen.getByText("param1")).toBeInTheDocument()
+ expect(screen.getByText("First parameter")).toBeInTheDocument()
+ })
+
+ it("grays out tool name and description when tool is disabled", () => {
+ const disabledTool = {
+ ...mockTool,
+ enabledForPrompt: false,
+ description: "A disabled tool",
+ }
+ render()
+
+ const toolName = screen.getByText("test-tool")
+ const toolDescription = screen.getByText("A disabled tool")
+
+ // Check that the tool name has the grayed out classes
+ expect(toolName).toHaveClass("text-vscode-descriptionForeground", "opacity-60")
+
+ // Check that the description has reduced opacity
+ expect(toolDescription).toHaveClass("opacity-40")
+ })
+
+ it("shows normal styling for tool name and description when tool is enabled", () => {
+ const enabledTool = {
+ ...mockTool,
+ enabledForPrompt: true,
+ description: "An enabled tool",
+ }
+ render()
+
+ const toolName = screen.getByText("test-tool")
+ const toolDescription = screen.getByText("An enabled tool")
+
+ // Check that the tool name has normal styling
+ expect(toolName).toHaveClass("text-vscode-foreground")
+ expect(toolName).not.toHaveClass("text-vscode-descriptionForeground", "opacity-60")
+
+ // Check that the description has normal opacity
+ expect(toolDescription).toHaveClass("opacity-80")
+ expect(toolDescription).not.toHaveClass("opacity-40")
+ })
})
diff --git a/webview-ui/src/components/ui/__tests__/toggle-switch.spec.tsx b/webview-ui/src/components/ui/__tests__/toggle-switch.spec.tsx
new file mode 100644
index 0000000000..e394e76139
--- /dev/null
+++ b/webview-ui/src/components/ui/__tests__/toggle-switch.spec.tsx
@@ -0,0 +1,101 @@
+import React from "react"
+import { render, fireEvent, screen } from "@/utils/test-utils"
+
+import { ToggleSwitch } from "../toggle-switch"
+
+describe("ToggleSwitch", () => {
+ it("renders with correct initial state", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ expect(toggle).toBeInTheDocument()
+ expect(toggle).toHaveAttribute("aria-checked", "true")
+ expect(toggle).toHaveAttribute("aria-label", "Test toggle")
+ })
+
+ it("renders unchecked state correctly", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ expect(toggle).toHaveAttribute("aria-checked", "false")
+ })
+
+ it("calls onChange when clicked", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ fireEvent.click(toggle)
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+
+ it("calls onChange when Enter key is pressed", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ fireEvent.keyDown(toggle, { key: "Enter" })
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+
+ it("calls onChange when Space key is pressed", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ fireEvent.keyDown(toggle, { key: " " })
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+
+ it("does not call onChange when disabled", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ fireEvent.click(toggle)
+ fireEvent.keyDown(toggle, { key: "Enter" })
+
+ expect(onChange).not.toHaveBeenCalled()
+ })
+
+ it("has correct tabIndex when disabled", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ expect(toggle).toHaveAttribute("tabindex", "-1")
+ })
+
+ it("renders with custom data-testid", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByTestId("custom-toggle")
+ expect(toggle).toBeInTheDocument()
+ })
+
+ it("supports medium size", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ expect(toggle).toBeInTheDocument()
+ // Medium size should be 20px x 10px
+ expect(toggle).toHaveStyle({ width: "20px", height: "10px" })
+ })
+
+ it("defaults to small size", () => {
+ const onChange = vi.fn()
+ render()
+
+ const toggle = screen.getByRole("switch")
+ expect(toggle).toBeInTheDocument()
+ // Small size should be 16px x 8px
+ expect(toggle).toHaveStyle({ width: "16px", height: "8px" })
+ })
+})
diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts
index c36d3b4769..ee28b964c5 100644
--- a/webview-ui/src/components/ui/index.ts
+++ b/webview-ui/src/components/ui/index.ts
@@ -18,3 +18,4 @@ export * from "./select"
export * from "./textarea"
export * from "./tooltip"
export * from "./standard-tooltip"
+export * from "./toggle-switch"
diff --git a/webview-ui/src/components/ui/toggle-switch.tsx b/webview-ui/src/components/ui/toggle-switch.tsx
new file mode 100644
index 0000000000..c2488851b5
--- /dev/null
+++ b/webview-ui/src/components/ui/toggle-switch.tsx
@@ -0,0 +1,68 @@
+import React from "react"
+
+export interface ToggleSwitchProps {
+ checked: boolean
+ onChange: () => void
+ disabled?: boolean
+ size?: "small" | "medium"
+ "aria-label"?: string
+ "data-testid"?: string
+}
+
+export const ToggleSwitch: React.FC = ({
+ checked,
+ onChange,
+ disabled = false,
+ size = "small",
+ "aria-label": ariaLabel,
+ "data-testid": dataTestId,
+}) => {
+ const dimensions = size === "small" ? { width: 16, height: 8, dotSize: 4 } : { width: 20, height: 10, dotSize: 6 }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault()
+ if (!disabled) {
+ onChange()
+ }
+ }
+ }
+
+ return (
+
+ )
+}