Skip to content

Commit 73c64d9

Browse files
Add Toggle MCP Servers Modal (RooCodeInc#2723)
* make ServerRow not optionally not expandable * changeset * factor out servers toggle list * changeset * add servers modal * changest * Reduce padding in modal * separate fetch useEffect for more efficient rendering --------- Co-authored-by: Saoud Rizwan <[email protected]>
1 parent 989eeb2 commit 73c64d9

File tree

7 files changed

+148
-17
lines changed

7 files changed

+148
-17
lines changed

.changeset/mean-pears-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add modal for toggling MCP servers to the chat area

.changeset/polite-lions-deliver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
factor out servers list

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/Api
2525
import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
2626
import ContextMenu from "@/components/chat/ContextMenu"
2727
import { ChatSettings } from "@shared/ChatSettings"
28+
import ServersToggleModal from "./ServersToggleModal"
2829

2930
interface ChatTextAreaProps {
3031
inputValue: string
@@ -1190,7 +1191,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
11901191
onClick={handleContextButtonClick}
11911192
style={{ padding: "0px 0px", height: "20px" }}>
11921193
<ButtonContainer>
1193-
<span style={{ fontSize: "13px", marginBottom: 1 }}>@</span>
1194+
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
1195+
@
1196+
</span>
11941197
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
11951198
</ButtonContainer>
11961199
</VSCodeButton>
@@ -1207,10 +1210,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12071210
}}
12081211
style={{ padding: "0px 0px", height: "20px" }}>
12091212
<ButtonContainer>
1210-
<span className="codicon codicon-device-camera" style={{ fontSize: "14px", marginBottom: -3 }} />
1213+
<span
1214+
className="codicon codicon-device-camera flex items-center"
1215+
style={{ fontSize: "14px", marginBottom: -3 }}
1216+
/>
12111217
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
12121218
</ButtonContainer>
12131219
</VSCodeButton>
1220+
<ServersToggleModal />
12141221

12151222
<ModelContainer ref={modelSelectorRef}>
12161223
<ModelButtonWrapper ref={buttonRef}>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useRef, useState, useEffect } from "react"
2+
import { useClickAway, useWindowSize } from "react-use"
3+
import { useExtensionState } from "@/context/ExtensionStateContext"
4+
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
5+
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
6+
import { vscode } from "@/utils/vscode"
7+
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
8+
9+
const ServersToggleModal: React.FC = () => {
10+
const { mcpServers } = useExtensionState()
11+
const [isVisible, setIsVisible] = useState(false)
12+
const buttonRef = useRef<HTMLDivElement>(null)
13+
const modalRef = useRef<HTMLDivElement>(null)
14+
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
15+
const [arrowPosition, setArrowPosition] = useState(0)
16+
const [menuPosition, setMenuPosition] = useState(0)
17+
18+
// Close modal when clicking outside
19+
useClickAway(modalRef, () => {
20+
setIsVisible(false)
21+
})
22+
23+
// Calculate positions for modal and arrow
24+
useEffect(() => {
25+
if (isVisible && buttonRef.current) {
26+
const buttonRect = buttonRef.current.getBoundingClientRect()
27+
const buttonCenter = buttonRect.left + buttonRect.width / 2
28+
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
29+
30+
setArrowPosition(rightPosition)
31+
setMenuPosition(buttonRect.top + 1)
32+
}
33+
}, [isVisible, viewportWidth, viewportHeight])
34+
35+
useEffect(() => {
36+
if (isVisible) {
37+
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
38+
}
39+
}, [isVisible])
40+
41+
return (
42+
<div ref={modalRef}>
43+
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
44+
<VSCodeButton
45+
appearance="icon"
46+
aria-label="MCP Servers"
47+
onClick={() => setIsVisible(!isVisible)}
48+
style={{ padding: "0px 0px", height: "20px" }}>
49+
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
50+
<span
51+
className="codicon codicon-server flex items-center"
52+
style={{ fontSize: "12.5px", marginBottom: 1 }}
53+
/>
54+
</div>
55+
</VSCodeButton>
56+
</div>
57+
58+
{isVisible && (
59+
<div
60+
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
61+
style={{
62+
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
63+
background: CODE_BLOCK_BG_COLOR,
64+
maxHeight: "calc(100vh - 100px)",
65+
overscrollBehavior: "contain",
66+
}}>
67+
<div
68+
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
69+
style={{
70+
bottom: `calc(100vh - ${menuPosition}px)`,
71+
right: arrowPosition,
72+
background: CODE_BLOCK_BG_COLOR,
73+
}}
74+
/>
75+
76+
<div className="m-0 mb-2.5">MCP Servers</div>
77+
<div style={{ marginBottom: "-10px" }}>
78+
<ServersToggleList servers={mcpServers} isExpandable={false} hasTrashIcon={false} listGap="small" />
79+
</div>
80+
</div>
81+
)}
82+
</div>
83+
)
84+
}
85+
86+
export default ServersToggleModal

webview-ui/src/components/mcp/configuration/tabs/installed/InstalledServersView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const InstalledServersView = () => {
2929
</VSCodeLink>
3030
</div>
3131

32-
<ServersToggleList servers={servers} />
32+
<ServersToggleList servers={servers} isExpandable={true} hasTrashIcon={false} />
3333

3434
{/* Settings Section */}
3535
<div style={{ marginBottom: "20px", marginTop: 10 }}>

webview-ui/src/components/mcp/configuration/tabs/installed/ServersToggleList.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import { McpServer } from "@shared/mcp"
22
import ServerRow from "./server-row/ServerRow"
33

4-
const ServersToggleList = ({ servers }: { servers: McpServer[] }) => {
4+
const ServersToggleList = ({
5+
servers,
6+
isExpandable,
7+
hasTrashIcon,
8+
listGap = "medium",
9+
}: {
10+
servers: McpServer[]
11+
isExpandable: boolean
12+
hasTrashIcon: boolean
13+
listGap?: "small" | "medium" | "large"
14+
}) => {
15+
const gapClasses = {
16+
small: "gap-0",
17+
medium: "gap-2.5",
18+
large: "gap-5",
19+
}
20+
21+
const gapClass = gapClasses[listGap]
22+
523
return servers.length > 0 ? (
6-
<div className="flex flex-col gap-2.5">
24+
<div className={`flex flex-col ${gapClass}`}>
725
{servers.map((server) => (
8-
<ServerRow key={server.name} server={server} />
26+
<ServerRow key={server.name} server={server} isExpandable={isExpandable} hasTrashIcon={hasTrashIcon} />
927
))}
1028
</div>
1129
) : (

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ import McpToolRow from "./McpToolRow"
1717
import McpResourceRow from "./McpResourceRow"
1818
import { useExtensionState } from "@/context/ExtensionStateContext"
1919

20-
const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpandable?: boolean }) => {
20+
const ServerRow = ({
21+
server,
22+
isExpandable = true,
23+
hasTrashIcon = true,
24+
}: {
25+
server: McpServer
26+
isExpandable?: boolean
27+
hasTrashIcon?: boolean
28+
}) => {
2129
const { mcpMarketplaceCatalog, autoApprovalSettings } = useExtensionState()
2230

2331
const [isExpanded, setIsExpanded] = useState(false)
@@ -138,16 +146,18 @@ const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpan
138146
disabled={server.status === "connecting"}>
139147
<span className="codicon codicon-sync"></span>
140148
</VSCodeButton>
141-
<VSCodeButton
142-
appearance="icon"
143-
title="Delete Server"
144-
onClick={(e) => {
145-
e.stopPropagation()
146-
handleDelete()
147-
}}
148-
disabled={isDeleting}>
149-
<span className="codicon codicon-trash"></span>
150-
</VSCodeButton>
149+
{hasTrashIcon && (
150+
<VSCodeButton
151+
appearance="icon"
152+
title="Delete Server"
153+
onClick={(e) => {
154+
e.stopPropagation()
155+
handleDelete()
156+
}}
157+
disabled={isDeleting}>
158+
<span className="codicon codicon-trash"></span>
159+
</VSCodeButton>
160+
)}
151161
</div>
152162
)}
153163
{/* Toggle Switch */}

0 commit comments

Comments
 (0)