Resizable left sidebar

Make left sidebar resizable so long URLs can be viewed in their
entirety.

Fixes https://github.com/modelcontextprotocol/inspector/issues/457

Vibe-coded with Cursor
This commit is contained in:
Marc Abramowitz 2025-05-27 17:28:04 -07:00
commit bff09e0409
3 changed files with 90 additions and 28 deletions

View file

@ -28,7 +28,7 @@ import React, {
useState,
} from "react";
import { useConnection } from "./lib/hooks/useConnection";
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
import { useDraggablePane, useDraggableSidebar } from "./lib/hooks/useDraggablePane";
import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -163,6 +163,7 @@ const App = () => {
const progressTokenRef = useRef(0);
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
const { width: sidebarWidth, isDragging: isSidebarDragging, handleDragStart: handleSidebarDragStart } = useDraggableSidebar(320);
const {
connectionStatus,
@ -562,32 +563,44 @@ const App = () => {
return (
<div className="flex h-screen bg-background">
<Sidebar
connectionStatus={connectionStatus}
transportType={transportType}
setTransportType={setTransportType}
command={command}
setCommand={setCommand}
args={args}
setArgs={setArgs}
sseUrl={sseUrl}
setSseUrl={setSseUrl}
env={env}
setEnv={setEnv}
config={config}
setConfig={setConfig}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}
logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false}
clearStdErrNotifications={clearStdErrNotifications}
/>
<div
style={{ width: sidebarWidth, minWidth: 200, maxWidth: 600, transition: isSidebarDragging ? 'none' : 'width 0.15s' }}
className="bg-card border-r border-border flex flex-col h-full relative"
>
<Sidebar
connectionStatus={connectionStatus}
transportType={transportType}
setTransportType={setTransportType}
command={command}
setCommand={setCommand}
args={args}
setArgs={setArgs}
sseUrl={sseUrl}
setSseUrl={setSseUrl}
env={env}
setEnv={setEnv}
config={config}
setConfig={setConfig}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}
logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false}
clearStdErrNotifications={clearStdErrNotifications}
/>
{/* Drag handle for resizing sidebar */}
<div
onMouseDown={handleSidebarDragStart}
style={{ cursor: 'col-resize', position: 'absolute', top: 0, right: 0, width: 6, height: '100%', zIndex: 10, background: isSidebarDragging ? 'rgba(0,0,0,0.08)' : 'transparent' }}
aria-label="Resize sidebar"
data-testid="sidebar-drag-handle"
/>
</div>
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
{mcpClient ? (

View file

@ -214,7 +214,7 @@ const Sidebar = ({
}, [generateMCPServerFile, toast, reportError]);
return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
<div className="bg-card border-r border-border flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center">
<h1 className="ml-2 text-lg font-semibold">

View file

@ -51,3 +51,52 @@ export function useDraggablePane(initialHeight: number) {
handleDragStart,
};
}
export function useDraggableSidebar(initialWidth: number) {
const [width, setWidth] = useState(initialWidth);
const [isDragging, setIsDragging] = useState(false);
const dragStartX = useRef<number>(0);
const dragStartWidth = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartX.current = e.clientX;
dragStartWidth.current = width;
document.body.style.userSelect = "none";
},
[width],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaX = e.clientX - dragStartX.current;
const newWidth = Math.max(200, Math.min(600, dragStartWidth.current + deltaX));
setWidth(newWidth);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
return {
width,
isDragging,
handleDragStart,
};
}