Browse Source

refactor: replace custom UI toggle with shadcn Switch component (#7308)

* refactor(webview-ui): replace custom UI toggle with shadcn Switch component

- Add @radix-ui/react-switch dependency (v1.2.6) https://ui.shadcn.com/docs/components/switch
- Refactor ClineRulesToggleModal to use Radix Switch instead of VSCode buttons
- Improve button styling with reduced padding and adjusted icon sizes
- Enhance form layout with conditional rendering based on expansion state
- Update input field styling with better focus states and border handling

This change provides a more consistent UI experience by leveraging Radix UI's
accessible Switch component while maintaining the same functionality.

* clean up

* clean up

* update switch color

* adjust

* revert unrelated changes

* size

* toggle

* Update webview-ui/src/components/cline-rules/RuleRow.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Bee 1 month ago
parent
commit
3baaa5c8b4

+ 1 - 1
src/core/controller/models/refreshRequestyModels.ts

@@ -59,4 +59,4 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
 	}
 
 	return OpenRouterCompatibleModelInfo.create({ models })
-}
+}

+ 45 - 0
webview-ui/package-lock.json

@@ -17,6 +17,7 @@
 				"@radix-ui/react-progress": "^1.1.7",
 				"@radix-ui/react-separator": "^1.1.7",
 				"@radix-ui/react-slot": "^1.2.3",
+				"@radix-ui/react-switch": "^1.2.6",
 				"@radix-ui/react-tooltip": "^1.2.8",
 				"@vscode/webview-ui-toolkit": "^1.4.0",
 				"class-variance-authority": "^0.7.1",
@@ -4132,6 +4133,35 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-switch": {
+			"version": "1.2.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+			"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.3",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-primitive": "2.1.3",
+				"@radix-ui/react-use-controllable-state": "1.2.2",
+				"@radix-ui/react-use-previous": "1.1.1",
+				"@radix-ui/react-use-size": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-tooltip": {
 			"version": "1.2.8",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
@@ -4251,6 +4281,21 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-use-previous": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+			"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-use-rect": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",

+ 1 - 0
webview-ui/package.json

@@ -25,6 +25,7 @@
 		"@radix-ui/react-progress": "^1.1.7",
 		"@radix-ui/react-separator": "^1.1.7",
 		"@radix-ui/react-slot": "^1.2.3",
+		"@radix-ui/react-switch": "^1.2.6",
 		"@radix-ui/react-tooltip": "^1.2.8",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
 		"class-variance-authority": "^0.7.1",

+ 20 - 20
webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx

@@ -81,7 +81,15 @@ const ClineRulesToggleModal: React.FC = () => {
 					console.error("Failed to refresh rules:", error)
 				})
 		}
-	}, [isVisible])
+	}, [
+		isVisible,
+		setGlobalClineRulesToggles,
+		setLocalClineRulesToggles,
+		setGlobalWorkflowToggles,
+		setLocalCursorRulesToggles,
+		setLocalWindsurfRulesToggles,
+		setLocalWorkflowToggles,
+	])
 
 	// Format global rules for display with proper typing
 	const globalRules = Object.entries(globalClineRulesToggles || {})
@@ -286,11 +294,9 @@ const ClineRulesToggleModal: React.FC = () => {
 						<VSCodeButton
 							appearance="icon"
 							aria-label={isVisible ? "Hide Cline Rules & Workflows" : "Show Cline Rules & Workflows"}
-							className="flex items-center"
+							className="p-0 m-0 flex items-center"
 							onClick={() => setIsVisible(!isVisible)}>
-							<div className="flex items-center text-xs w-full cursor-pointer">
-								<i className="codicon codicon-law" style={{ fontSize: "14px" }} />
-							</div>
+							<i className="codicon codicon-law" style={{ fontSize: "12.5px" }} />
 						</VSCodeButton>
 					</TooltipTrigger>
 				</Tooltip>
@@ -298,7 +304,7 @@ const ClineRulesToggleModal: React.FC = () => {
 
 			{isVisible && (
 				<div
-					className="fixed left-[15px] right-[15px] border border-(--vscode-editorGroup-border) p-3 rounded z-1000 overflow-y-auto"
+					className="fixed left-[15px] right-[15px] border border-editor-group-border pb-3 px-2 rounded z-1000"
 					style={{
 						bottom: `calc(100vh - ${menuPosition}px + 6px)`,
 						background: CODE_BLOCK_BG_COLOR,
@@ -306,7 +312,7 @@ const ClineRulesToggleModal: React.FC = () => {
 						overscrollBehavior: "contain",
 					}}>
 					<div
-						className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-(--vscode-editorGroup-border)"
+						className="fixed h-2.5 w-2.5 z-[-1] rotate-45 border-r border-b border-editor-group-border"
 						style={{
 							bottom: `calc(100vh - ${menuPosition}px)`,
 							right: arrowPosition,
@@ -337,19 +343,19 @@ const ClineRulesToggleModal: React.FC = () => {
 					</div>
 
 					{/* Remote config banner */}
-					{((currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows)) && (
+					{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
 						<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
 							<i className="codicon codicon-lock text-sm" />
-							<span className="text-[13px]">
+							<span className="text-base">
 								{currentView === "rules"
 									? "Your organization manages some rules"
 									: "Your organization manages some workflows"}
 							</span>
 						</div>
-					)}
+					) : null}
 
 					{/* Description text */}
-					<div className="text-xs text-(--vscode-descriptionForeground) mb-4">
+					<div className="text-xs text-description mb-4">
 						{currentView === "rules" ? (
 							<p>
 								Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
@@ -365,16 +371,10 @@ const ClineRulesToggleModal: React.FC = () => {
 							<p>
 								Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
 								such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
-								<span
-									className=" 
-								text-(--vscode-foreground) font-bold">
-									/workflow-name
-								</span>{" "}
-								in the chat.{" "}
+								<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
 								<VSCodeLink
-									className="text-xs"
-									href="https://docs.cline.bot/features/slash-commands/workflows"
-									style={{ display: "inline" }}>
+									className="text-xs inline"
+									href="https://docs.cline.bot/features/slash-commands/workflows">
 									Docs
 								</VSCodeLink>
 							</p>

+ 56 - 53
webview-ui/src/components/cline-rules/NewRuleRow.tsx

@@ -1,7 +1,9 @@
 import { RuleFileRequest } from "@shared/proto/index.cline"
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+import { PlusIcon } from "lucide-react"
 import { useEffect, useRef, useState } from "react"
 import { useClickAway } from "react-use"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
 import { FileServiceClient } from "@/services/grpc-client"
 
 interface NewRuleRowProps {
@@ -88,65 +90,66 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal, ruleType }) => {
 
 	return (
 		<div
-			className={`mb-2.5 transition-all duration-300 ease-in-out ${isExpanded ? "opacity-100" : "opacity-70 hover:opacity-100"}`}
+			className={cn("mb-2.5 transition-all duration-300 ease-in-out", {
+				"opacity-100": isExpanded,
+				"opacity-70 hover:opacity-100": !isExpanded,
+			})}
 			onClick={() => !isExpanded && setIsExpanded(true)}
 			ref={componentRef}>
 			<div
-				className={`flex items-center p-2 py-4 rounded bg-input-background transition-all duration-300 ease-in-out h-[18px] ${
-					isExpanded ? "shadow-sm" : ""
-				}`}>
-				{isExpanded ? (
-					<form className="flex flex-1 items-center" onSubmit={handleSubmit}>
-						<input
-							className="flex-1 bg-input-background text-(--vscode-input-foreground) border-0 outline-0 rounded focus:outline-none focus:ring-0 focus:border-transparent"
-							onChange={(e) => setFilename(e.target.value)}
-							onKeyDown={handleKeyDown}
-							placeholder={
-								ruleType === "workflow"
+				className={cn(
+					"flex items-center px-2 py-4 rounded bg-input-background transition-all duration-300 ease-in-out h-5",
+					{
+						"shadow-sm": isExpanded,
+					},
+				)}>
+				<form className="flex flex-1 items-center" onSubmit={handleSubmit}>
+					<input
+						className={cn(
+							"flex-1 bg-input-background text-input-foreground border-0 outline-0 rounded focus:outline-none focus:ring-0 focus:border-transparent",
+							{
+								italic: !isExpanded,
+							},
+						)}
+						onChange={(e) => setFilename(e.target.value)}
+						placeholder={
+							isExpanded
+								? ruleType === "workflow"
 									? "workflow-name (.md, .txt, or no extension)"
 									: "rule-name (.md, .txt, or no extension)"
+								: ruleType === "workflow"
+									? "New workflow file..."
+									: "New rule file..."
+						}
+						ref={inputRef}
+						type="text"
+						value={isExpanded ? filename : ""}
+					/>
+
+					<Button
+						aria-label={
+							isExpanded
+								? "Create rule file"
+								: ruleType === "workflow"
+									? "New workflow file..."
+									: "New rule file..."
+						}
+						className="mx-0.5"
+						onClick={(e) => {
+							e.stopPropagation()
+							if (!isExpanded) {
+								setIsExpanded(true)
 							}
-							ref={inputRef}
-							style={{
-								outline: "none",
-							}}
-							type="text"
-							value={filename}
-						/>
-
-						<div className="flex items-center ml-2 space-x-2">
-							<VSCodeButton
-								appearance="icon"
-								aria-label="Create rule file"
-								style={{ padding: "0px" }}
-								title="Create rule file"
-								type="submit">
-								<span className="codicon codicon-add text-[14px]" />
-							</VSCodeButton>
-						</div>
-					</form>
-				) : (
-					<>
-						<span className="flex-1 text-(--vscode-descriptionForeground) bg-input-background italic text-xs">
-							{ruleType === "workflow" ? "New workflow file..." : "New rule file..."}
-						</span>
-						<div className="flex items-center ml-2 space-x-2">
-							<VSCodeButton
-								appearance="icon"
-								aria-label={ruleType === "workflow" ? "New workflow file..." : "New rule file..."}
-								onClick={(e) => {
-									e.stopPropagation()
-									setIsExpanded(true)
-								}}
-								style={{ padding: "0px" }}
-								title="New rule file">
-								<span className="codicon codicon-add text-[14px]" />
-							</VSCodeButton>
-						</div>
-					</>
-				)}
+						}}
+						size="icon"
+						title={isExpanded ? "Create rule file" : "New rule file"}
+						type={isExpanded ? "submit" : "button"}
+						variant="icon">
+						<PlusIcon />
+					</Button>
+				</form>
 			</div>
-			{isExpanded && error && <div className="text-(--vscode-errorForeground) text-xs mt-1 ml-2">{error}</div>}
+			{isExpanded && error && <div className="text-error text-xs mt-1 ml-2">{error}</div>}
 		</div>
 	)
 }

+ 36 - 54
webview-ui/src/components/cline-rules/RuleRow.tsx

@@ -1,7 +1,10 @@
 import { StringRequest } from "@shared/proto/cline/common"
 import { RuleFileRequest } from "@shared/proto/index.cline"
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+import { InfoIcon, PenIcon, Trash2Icon } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Switch } from "@/components/ui/switch"
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
+import { cn } from "@/lib/utils"
 import { FileServiceClient } from "@/services/grpc-client"
 
 const RuleRow: React.FC<{
@@ -96,18 +99,16 @@ const RuleRow: React.FC<{
 	return (
 		<div className="mb-2.5">
 			<div
-				className={`flex items-center p-2 py-4 rounded bg-(--vscode-textCodeBlock-background) h-[18px] ${
-					enabled ? "opacity-100" : "opacity-60"
-				} ${isDisabled ? "opacity-50" : ""}`}>
+				className={cn("flex items-center px-2 py-4 rounded bg-text-block-background max-h-4", {
+					'opacity-60': isDisabled,
+				})}>
 				<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
 					{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
 					<span className="ph-no-capture">{finalDisplayName}</span>
 					{ruleType === "agents" && (
 						<Tooltip>
-							<TooltipTrigger asChild>
-								<span className="mt-1 ml-1.5 cursor-help">
-									<i className="codicon codicon-info" style={{ fontSize: "12px", opacity: 0.7 }} />
-								</span>
+							<TooltipTrigger asChild className="cursor-help">
+								<InfoIcon className="ml-1.5 opacity-70 size-[0.85rem]" />
 							</TooltipTrigger>
 							<TooltipContent>
 								Searches recursively for all AGENTS.md files in the workspace when a top-level AGENTS.md exists
@@ -117,52 +118,33 @@ const RuleRow: React.FC<{
 				</span>
 
 				{/* Toggle Switch */}
-				<div className="flex items-center ml-2 space-x-2">
-					<div
-						aria-checked={enabled}
-						className={`w-[20px] h-[10px] rounded-[5px] relative transition-colors duration-200 outline-none focus:outline-none ${
-							isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
-						} ${
-							enabled
-								? "bg-(--vscode-testing-iconPassed) opacity-90"
-								: "bg-(--vscode-titleBar-inactiveForeground) opacity-50"
-						}`}
-						onClick={() => !isDisabled && toggleRule(rulePath, !enabled)}
-						onKeyDown={(e) => {
-							if (!isDisabled && (e.key === "Enter" || e.key === " ")) {
-								e.preventDefault()
-								toggleRule(rulePath, !enabled)
-							}
-						}}
-						role="switch"
-						tabIndex={isDisabled ? -1 : 0}
-						title={isDisabled ? "This rule is required and cannot be disabled" : undefined}>
-						<div
-							className={`w-[8px] h-[8px] bg-white border border-[#66666699] rounded-full absolute top-[1px] transition-all duration-200 pointer-events-none ${
-								enabled ? "left-[11px]" : "left-[1px]"
-							}`}
-						/>
-					</div>
-					{!isRemote && (
-						<>
-							<VSCodeButton
-								appearance="icon"
-								aria-label="Edit rule file"
-								onClick={handleEditClick}
-								style={{ height: "20px" }}
-								title="Edit rule file">
-								<span className="codicon codicon-edit" style={{ fontSize: "14px" }} />
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="icon"
-								aria-label="Delete rule file"
-								onClick={handleDeleteClick}
-								style={{ height: "20px" }}
-								title="Delete rule file">
-								<span className="codicon codicon-trash" style={{ fontSize: "14px" }} />
-							</VSCodeButton>
-						</>
-					)}
+				<div className="flex items-center space-x-2 gap-2">
+					<Switch
+						checked={enabled}
+						className="mx-1"
+						disabled={isRemote}
+						key={rulePath}
+						onClick={() => toggleRule(rulePath, !enabled)}
+						title={isDisabled ? "This rule is required and cannot be disabled" : undefined}
+					/>
+					<Button
+						aria-label="Edit rule file"
+						disabled={isRemote}
+						onClick={handleEditClick}
+						size="xs"
+						title="Edit rule file"
+						variant="icon">
+						<PenIcon />
+					</Button>
+					<Button
+						aria-label="Delete rule file"
+						disabled={isRemote}
+						onClick={handleDeleteClick}
+						size="xs"
+						title="Delete rule file"
+						variant="icon">
+						<Trash2Icon />
+					</Button>
 				</div>
 			</div>
 		</div>

+ 44 - 108
webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx

@@ -16,9 +16,13 @@ import {
 	VSCodePanelTab,
 	VSCodePanelView,
 } from "@vscode/webview-ui-toolkit/react"
-import { useCallback, useState } from "react"
+import { RefreshCcwIcon, Trash2Icon } from "lucide-react"
+import { useState } from "react"
 import DangerButton from "@/components/common/DangerButton"
+import { Button } from "@/components/ui/button"
+import { Switch } from "@/components/ui/switch"
 import { useExtensionState } from "@/context/ExtensionStateContext"
+import { cn } from "@/lib/utils"
 import { McpServiceClient } from "@/services/grpc-client"
 import { getMcpServerDisplayName } from "@/utils/mcp"
 import McpResourceRow from "./McpResourceRow"
@@ -53,17 +57,6 @@ const ServerRow = ({
 	const [isDeleting, setIsDeleting] = useState(false)
 	const [isRestarting, setIsRestarting] = useState(false)
 
-	const getStatusColor = useCallback((status: McpServer["status"]) => {
-		switch (status) {
-			case "connected":
-				return "var(--vscode-testing-iconPassed)"
-			case "connecting":
-				return "var(--vscode-charts-yellow)"
-			case "disconnected":
-				return "var(--vscode-testing-iconFailed)"
-		}
-	}, [])
-
 	const handleRowClick = () => {
 		if (!server.error && isExpandable) {
 			setIsExpanded(!isExpanded)
@@ -173,111 +166,54 @@ const ServerRow = ({
 	}
 
 	return (
-		<div style={{ marginBottom: "10px" }}>
-			<div
-				onClick={handleRowClick}
-				style={{
-					display: "flex",
-					alignItems: "center",
-					padding: "8px",
-					background: "var(--vscode-textCodeBlock-background)",
-
-					cursor: server.error ? "default" : isExpandable ? "pointer" : "default",
-					borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
-					opacity: server.disabled ? 0.6 : 1,
-				}}>
+		<div className="mb-2.5">
+			<div className="flex bg-code-block-background p-2 gap-4 items-center" onClick={handleRowClick}>
 				{!server.error && isExpandable && (
-					<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
+					<span
+						className={cn("mr-2 codicon", {
+							"codicon-chevron-right": !isExpanded,
+							"codicon-chevron-down": isExpanded,
+						})}
+					/>
 				)}
-				<span
-					style={{
-						flex: 1,
-						overflow: "hidden",
-						wordBreak: "break-all",
-						whiteSpace: "normal",
-						display: "flex",
-						alignItems: "center",
-						marginRight: "4px",
-					}}>
+				<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center">
 					{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
 				</span>
 				{/* Collapsed view controls */}
 				{!server.error && (
-					<div style={{ display: "flex", alignItems: "center", gap: "4px", marginLeft: "8px" }}>
-						<VSCodeButton
-							appearance="icon"
-							disabled={server.status === "connecting" || isRestarting}
-							onClick={(e) => {
-								e.stopPropagation()
-								handleRestart()
-							}}
-							title="Restart Server">
-							<span className="codicon codicon-sync"></span>
-						</VSCodeButton>
-						{hasTrashIcon && (
-							<VSCodeButton
-								appearance="icon"
-								disabled={isDeleting}
-								onClick={(e) => {
-									e.stopPropagation()
-									handleDelete()
-								}}
-								title="Delete Server">
-								<span className="codicon codicon-trash"></span>
-							</VSCodeButton>
-						)}
-					</div>
-				)}
-				{/* Toggle Switch */}
-				<div onClick={(e) => e.stopPropagation()} style={{ display: "flex", alignItems: "center", marginLeft: "8px" }}>
-					<div
-						aria-checked={!server.disabled}
-						onClick={() => {
-							handleToggleMcpServer()
+					<Button
+						disabled={server.status === "connecting" || isRestarting || server.disabled}
+						onClick={(e) => {
+							e.stopPropagation()
+							handleRestart()
 						}}
-						onKeyDown={(e) => {
-							if (e.key === "Enter" || e.key === " ") {
-								e.preventDefault()
-								handleToggleMcpServer()
-							}
-						}}
-						role="switch"
-						style={{
-							width: "20px",
-							height: "10px",
-							backgroundColor: server.disabled
-								? "var(--vscode-titleBar-inactiveForeground)"
-								: "var(--vscode-testing-iconPassed)",
-							borderRadius: "5px",
-							position: "relative",
-							cursor: "pointer",
-							transition: "background-color 0.2s",
-							opacity: server.disabled ? 0.5 : 0.9,
+						size="icon"
+						title="Restart Server"
+						variant="icon">
+						<RefreshCcwIcon />
+					</Button>
+				)}
+				{!server.error && hasTrashIcon && (
+					<Button
+						disabled={isDeleting}
+						onClick={(e) => {
+							e.stopPropagation()
+							handleDelete()
 						}}
-						tabIndex={0}>
-						<div
-							style={{
-								width: "6px",
-								height: "6px",
-								backgroundColor: "white",
-								border: "1px solid color-mix(in srgb, #666666 65%, transparent)",
-								borderRadius: "50%",
-								position: "absolute",
-								top: "1px",
-								left: server.disabled ? "2px" : "12px",
-								transition: "left 0.2s",
-							}}
-						/>
-					</div>
-				</div>
+						size="icon"
+						title="Delete Server"
+						variant="icon">
+						<Trash2Icon />
+					</Button>
+				)}
+				{/* Toggle Switch */}
+				<Switch checked={!server.disabled} key={server.name} onClick={handleToggleMcpServer} />
 				<div
-					style={{
-						width: "8px",
-						height: "8px",
-						borderRadius: "50%",
-						background: getStatusColor(server.status),
-						marginLeft: "8px",
-					}}
+					className={cn("h-2 w-2 ml-0.5 rounded-full", {
+						"bg-success": server.status === "connected",
+						"bg-warning": server.status === "connecting",
+						"bg-error": server.status === "disconnected",
+					})}
 				/>
 			</div>
 

+ 26 - 0
webview-ui/src/components/ui/switch.tsx

@@ -0,0 +1,26 @@
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+	React.ElementRef<typeof SwitchPrimitives.Root>,
+	React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
+>(({ className, ...props }, ref) => (
+	<SwitchPrimitives.Root
+		className={cn(
+			"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-button-background/60 data-[state=unchecked]:bg-input-foreground/50",
+			className,
+		)}
+		{...props}
+		ref={ref}>
+		<SwitchPrimitives.Thumb
+			className={cn(
+				"pointer-events-none block h-2 w-2 rounded-full bg-button-foreground/80 shadow-lg transition-transform data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
+			)}
+		/>
+	</SwitchPrimitives.Root>
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }

+ 2 - 0
webview-ui/src/theme.css

@@ -9,6 +9,8 @@
 	--color-code-foreground: var(--vscode-editor-foreground);
 	--color-code-border: var(--vscode-editor-border);
 	--color-code-block-background: var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30));
+	--color-text-block-background: var(--vscode-textCodeBlock-background);
+	--color-editor-group-border: var(--vscode-editorGroup-border);
 	--color-sidebar-background: var(--vscode-sideBar-background);
 	--color-sidebar-foreground: var(--vscode-sideBar-foreground);
 	--color-input-foreground: var(--vscode-input-foreground);