update ui to look like vs marketplace

This commit is contained in:
Saoud Rizwan 2025-02-15 14:33:58 -08:00
parent e3b4b2c816
commit 0cbd6b321f
3 changed files with 360 additions and 184 deletions

View file

@ -13,6 +13,7 @@ import { McpServer } from "../../../../src/shared/mcp"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
import styled from "styled-components"
type McpViewProps = {
onDone: () => void
@ -20,7 +21,7 @@ type McpViewProps = {
const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers } = useExtensionState()
const [activeTab, setActiveTab] = useState(servers.length === 0 ? "marketplace" : "installed")
const [activeTab, setActiveTab] = useState("marketplace")
// const [servers, setServers] = useState<McpServer[]>([
// // Add some mock servers for testing
@ -99,20 +100,34 @@ const McpView = ({ onDone }: McpViewProps) => {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 17px 10px 20px",
padding: "10px 17px 5px 20px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<VSCodePanels activeid={activeTab} onchange={(e: any) => setActiveTab(e.target.activeid)}>
<VSCodePanelTab id="installed">Installed</VSCodePanelTab>
<VSCodePanelTab id="marketplace">Marketplace</VSCodePanelTab>
<VSCodePanelTab id="settings">Settings</VSCodePanelTab>
<div style={{ flex: 1, overflow: "auto" }}>
{/* Tabs container */}
<div
style={{
display: "flex",
gap: "1px",
padding: "0 8px 0 10px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
<TabButton isActive={activeTab === "marketplace"} onClick={() => setActiveTab("marketplace")}>
Marketplace
</TabButton>
<TabButton isActive={activeTab === "installed"} onClick={() => setActiveTab("installed")}>
Installed
</TabButton>
</div>
<VSCodePanelView id="installed">
<div style={{ padding: "0 20px" }}>
{/* Content container */}
<div style={{ width: "100%" }}>
{activeTab === "marketplace" && <McpMarketplaceView />}
{activeTab === "installed" && (
<div style={{ padding: "16px 20px" }}>
<div
style={{
color: "var(--vscode-foreground)",
@ -164,51 +179,62 @@ const McpView = ({ onDone }: McpViewProps) => {
</VSCodeButton>
</div>
)}
</div>
</VSCodePanelView>
<VSCodePanelView id="marketplace">
<div style={{ maxWidth: "100%" }}>
<McpMarketplaceView />
</div>
</VSCodePanelView>
<VSCodePanelView id="settings">
<div style={{ maxWidth: "100%" }}>
{/* Server Configuration Button */}
<div style={{ marginTop: "10px", width: "100%" }}>
{/* Settings Section */}
<div style={{ marginBottom: "20px", marginTop: 10 }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%" }}
style={{ width: "100%", marginBottom: "5px" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
</div>
{/* Advanced Settings Link */}
<div style={{ textAlign: "center", marginTop: "5px" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
<div style={{ textAlign: "center" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
</div>
</div>
</VSCodePanelView>
</VSCodePanels>
)}
</div>
</div>
</div>
)
}
const StyledTabButton = styled.button<{ isActive: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
margin-bottom: -1px;
font-family: inherit;
&:hover {
color: var(--vscode-foreground);
}
`
const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
{children}
</StyledTabButton>
)
// Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => {
const [isExpanded, setIsExpanded] = useState(false)

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { CSSProperties, useEffect, useState } from "react"
import styled from "styled-components"
import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp"
import { vscode } from "../../../utils/vscode"
@ -27,40 +28,62 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
}, [])
return (
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "4px",
padding: "16px",
display: "flex",
flexDirection: "column",
gap: "12px",
}}>
<div>
<div style={{ display: "flex", gap: "16px", alignItems: "flex-start" }}>
<>
<style>
{`
.mcp-card {
cursor: pointer;
}
.mcp-card:hover {
background-color: var(--vscode-list-hoverBackground);
}
`}
</style>
<div
className="mcp-card"
onClick={() => {
console.log("Card clicked:", item.mcpId)
}}
style={{
borderBottom: "1px solid var(--vscode-textCodeBlock-background)",
padding: "12px 16px",
}}>
{/* Main container with logo and content */}
<div style={{ display: "flex", gap: "12px" }}>
{/* Logo */}
{item.logoUrl && (
<img
src={item.logoUrl}
alt={`${item.name} logo`}
style={{
width: "48px",
height: "48px",
width: "32px",
height: "32px",
borderRadius: "4px",
}}
/>
)}
<div style={{ flex: 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div>
<h3 style={{ margin: 0, fontSize: "16px" }}>{item.name}</h3>
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
by {item.author}
</div>
</div>
<VSCodeButton
appearance={isInstalled ? "secondary" : "primary"}
disabled={isInstalled || isDownloading}
onClick={() => {
{/* Content section */}
<div style={{ flex: 1, minWidth: 0 }}>
{/* First row: name and install button */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "12px",
}}>
<h3
style={{
margin: 0,
fontSize: "13px",
fontWeight: 600,
}}>
{item.name}
</h3>
<div
onClick={(e) => {
e.stopPropagation() // Prevent card click when clicking install
if (!isInstalled && !isDownloading) {
setIsDownloading(true)
vscode.postMessage({
@ -68,22 +91,25 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
mcpId: item.mcpId,
})
}
}}>
<span
className={`codicon codicon-${isInstalled ? "check" : isDownloading ? "sync codicon-modifier-spin" : "cloud-download"}`}
style={{ marginRight: "6px" }}
/>
{isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"}
</VSCodeButton>
}}
style={{}}>
<StyledInstallButton disabled={isInstalled || isDownloading} $isInstalled={isInstalled}>
{isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"}
</StyledInstallButton>
</div>
</div>
<p style={{ margin: "8px 0", fontSize: "13px" }}>{item.description}</p>
{/* Second row: metadata */}
<div
style={{
display: "flex",
gap: "16px",
alignItems: "center",
gap: "6px",
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
alignItems: "center",
marginTop: "2px",
flexWrap: "wrap",
minWidth: 0,
}}>
<VSCodeLink
href={item.githubUrl}
@ -91,60 +117,137 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
style={{
display: "flex",
alignItems: "center",
height: "16px", // hacky way to get the icon to center. vscodelink does weird stuff
color: "var(--vscode-descriptionForeground)",
marginBottom: "-3px",
minWidth: 0,
}}>
<span className="codicon codicon-github" style={{ fontSize: "16px" }} />
<span className="codicon codicon-github" style={{ fontSize: "14px" }} />
</VSCodeLink>
<div style={{ display: "flex", alignItems: "center", gap: "4px", marginRight: "8px" }}>
<span className="codicon codicon-star-full" />
{item.githubStars?.toLocaleString() ?? 0}
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span className="codicon codicon-cloud-download" />
{item.downloadCount?.toLocaleString() ?? 0}
</div>
{item.requiresApiKey && (
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span className="codicon codicon-key" title="Requires API key" />
</div>
)}
{item.isRecommended && (
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span className="codicon codicon-verified" title="Recommended" />
</div>
)}
</div>
<div style={{ display: "flex", gap: "8px", marginTop: "8px", flexWrap: "wrap" }}>
<span
style={{
fontSize: "11px",
padding: "2px 6px",
borderRadius: "4px",
background: "var(--vscode-charts-blue)",
color: "var(--vscode-foreground)",
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-all",
minWidth: 0,
}}>
{item.category}
{item.author}
</span>
{item.tags.map((tag) => (
<span
key={tag}
style={{
fontSize: "11px",
padding: "2px 6px",
borderRadius: "4px",
background: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
}}>
{tag}
</span>
))}
<span style={{ color: "var(--vscode-descriptionForeground)", flexShrink: 0 }}>|</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-star-full" />
<span style={{ wordBreak: "break-all" }}>{item.githubStars?.toLocaleString() ?? 0}</span>
</div>
<span style={{ color: "var(--vscode-descriptionForeground)", flexShrink: 0 }}>|</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-cloud-download" />
<span style={{ wordBreak: "break-all" }}>{item.downloadCount?.toLocaleString() ?? 0}</span>
</div>
{item.requiresApiKey && (
<span className="codicon codicon-key" title="Requires API key" style={{ flexShrink: 0 }} />
)}
{item.isRecommended && (
<span className="codicon codicon-verified" title="Recommended" style={{ flexShrink: 0 }} />
)}
</div>
</div>
</div>
{/* Description and tags */}
<div style={{ marginTop: "8px" }}>
<p style={{ margin: "0 0 4px 0", fontSize: "13px" }}>{item.description}</p>
<div
style={{
display: "flex",
gap: "6px",
flexWrap: "nowrap",
overflow: "hidden",
position: "relative",
}}>
<span
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
{item.category}
</span>
{item.tags.map((tag, index) => (
<span
key={tag}
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
display: "inline-flex",
}}>
{tag}
{index === item.tags.length - 1 ? "" : ""}
</span>
))}
<div
style={{
position: "absolute",
right: 0,
top: 0,
bottom: 0,
width: "32px",
background: "linear-gradient(to right, transparent, var(--vscode-editor-background))",
pointerEvents: "none",
}}
/>
</div>
</div>
</div>
</div>
</>
)
}
const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>`
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 2px;
border: none;
cursor: pointer;
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
color: var(--vscode-button-foreground);
&:hover:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryHoverBackground)" : "var(--vscode-button-hoverBackground)"};
}
&:active:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
opacity: 0.7;
}
&:disabled {
opacity: 0.5;
cursor: default;
}
`
export default McpMarketplaceCard

View file

@ -1,5 +1,13 @@
import { useEffect, useMemo, useState } from "react"
import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import {
VSCodeButton,
VSCodeProgressRing,
VSCodeRadioGroup,
VSCodeRadio,
VSCodeDropdown,
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
@ -33,11 +41,6 @@ const selectStyles = {
cursor: "pointer", // Show pointer cursor on hover
} as const
const refreshStyles = {
height: controlHeight,
alignSelf: "flex-end",
} as const
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
@ -46,7 +49,7 @@ const McpMarketplaceView = () => {
const [isRefreshing, setIsRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount")
const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name" | "newest">("downloadCount")
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
@ -72,6 +75,8 @@ const McpMarketplaceView = () => {
return b.githubStars - a.githubStars
case "name":
return a.name.localeCompare(b.name)
case "newest":
return b.githubStars - a.githubStars // FIXME: b.createdAt - a.createdAt // Assuming there's a createdAt field
default:
return 0
}
@ -154,73 +159,115 @@ const McpMarketplaceView = () => {
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "16px",
gap: "12px",
flexWrap: "wrap",
}}>
<div style={{ display: "flex", gap: "12px", alignItems: "center", flexWrap: "wrap", minWidth: 0 }}>
{" "}
{/* Added minWidth: 0 to prevent flex item from overflowing */}
<div style={{ position: "relative", flex: 1 }}>
<input
type="text"
placeholder="Search MCPs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="mcp-search-input"
style={searchInputStyles}
/>
<span
className="codicon codicon-search"
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
}}>
<div style={{ padding: "16px 16px 5px 16px" }}>
{/* Search row */}
<VSCodeTextField
style={{ width: "100%" }}
placeholder="Search MCPs..."
value={searchQuery}
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}>
<div
slot="start"
className="codicon codicon-search"
style={{
fontSize: 13,
marginTop: 2.5,
opacity: 0.8,
}}
/>
{searchQuery && (
<div
className="codicon codicon-close"
aria-label="Clear search"
onClick={() => setSearchQuery("")}
slot="end"
style={{
position: "absolute",
left: "10px",
top: "50%",
transform: "translateY(-50%)",
color: "var(--vscode-input-placeholderForeground)",
pointerEvents: "none",
fontSize: "14px", // Match input text size
lineHeight: 1, // Ensure icon is centered properly
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
cursor: "pointer",
}}
/>
)}
</VSCodeTextField>
{/* Filter row */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
}}>
Filter:
</span>
<div
style={{
// transform: "scale(0.9)",
// transformOrigin: "left center",
position: "relative",
zIndex: 2,
}}>
<VSCodeDropdown
value={selectedCategory || ""}
onChange={(e) => setSelectedCategory((e.target as HTMLSelectElement).value || null)}>
<VSCodeOption value="">All Categories</VSCodeOption>
{categories.map((category) => (
<VSCodeOption key={category} value={category}>
{category}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<select
value={selectedCategory || ""}
onChange={(e) => setSelectedCategory(e.target.value || null)}
className="mcp-select"
style={selectStyles}>
<option value="">All Categories</option>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as "downloadCount" | "stars" | "name")}
className="mcp-select"
style={selectStyles}>
<option value="downloadCount">Sort by Downloads</option>
<option value="stars">Sort by Stars</option>
<option value="name">Sort by Name</option>
</select>
</div>
<VSCodeButton
appearance="secondary"
onClick={() => fetchMarketplace(true)}
disabled={isRefreshing}
style={refreshStyles}>
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
Refresh
</VSCodeButton>
{/* Sort row */}
<div
style={{
display: "flex",
gap: "8px",
marginTop: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
marginTop: "3px",
}}>
Sort:
</span>
<VSCodeRadioGroup
style={{
display: "flex",
flexWrap: "wrap",
marginTop: "-2.5px",
}}
value={sortBy}
onChange={(e) => setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}>
<VSCodeRadio value="downloadCount">Most Installs</VSCodeRadio>
<VSCodeRadio value="stars">Most Stars</VSCodeRadio>
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="name">Name</VSCodeRadio>
</VSCodeRadioGroup>
</div>
</div>
<style>
{`
.mcp-search-input,