diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index febea1d8f..40ff8d060 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -42,6 +42,7 @@ import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; import { useToast } from "@/lib/hooks/useToast"; import useCopy from "@/lib/hooks/useCopy"; +import { useDraggableHorizontalPane } from "@/lib/hooks/useDraggablePane"; import IconDisplay, { WithIcons } from "./IconDisplay"; import { cn } from "@/lib/utils"; import { @@ -210,6 +211,14 @@ const ToolsTab = ({ const formRefs = useRef>({}); const { toast } = useToast(); const { copied, setCopied } = useCopy(); + // Draggable horizontal splitter between the tools list and the tool details/result pane. + // Mirrors the existing History pane resizer (`useDraggablePane`) so the top panes can be + // resized too, addressing modelcontextprotocol/inspector#1172. + const { + width: listPaneWidth, + isDragging: isListPaneDragging, + handleDragStart: handleListPaneDragStart, + } = useDraggableHorizontalPane(360, 240, 800); // Function to check if any form has validation errors const checkValidationErrors = (validateChildren: boolean = false) => { @@ -274,36 +283,53 @@ const ToolsTab = ({ return ( -
- { - clearTools(); - setSelectedTool(null); - setRunAsTask(false); - }} - setSelectedItem={setSelectedTool} - renderItem={(tool) => ( -
-
- -
-
- {tool.title || tool.name} - - {tool.description} - +
+
+ { + clearTools(); + setSelectedTool(null); + setRunAsTask(false); + }} + setSelectedItem={setSelectedTool} + renderItem={(tool) => ( +
+
+ +
+
+ {tool.title || tool.name} + + {tool.description} + +
+
- -
- )} - title="Tools" - buttonText={nextCursor ? "List More Tools" : "List Tools"} - isButtonDisabled={!nextCursor && tools.length > 0} - /> - -
+ )} + title="Tools" + buttonText={nextCursor ? "List More Tools" : "List Tools"} + isButtonDisabled={!nextCursor && tools.length > 0} + /> +
+
+
+
+
{selectedTool && ( diff --git a/client/src/lib/hooks/useDraggablePane.ts b/client/src/lib/hooks/useDraggablePane.ts index 4ee5af541..88b8b0c71 100644 --- a/client/src/lib/hooks/useDraggablePane.ts +++ b/client/src/lib/hooks/useDraggablePane.ts @@ -103,3 +103,62 @@ export function useDraggableSidebar(initialWidth: number) { handleDragStart, }; } + +// Generic horizontal-resize hook for tab inner splitters (list pane <-> details pane). +// Same pattern as useDraggableSidebar but with caller-supplied min/max so individual +// tabs can choose sensible bounds. +export function useDraggableHorizontalPane( + initialWidth: number, + minWidth: number = 200, + maxWidth: number = 1200, +) { + const [width, setWidth] = useState(initialWidth); + const [isDragging, setIsDragging] = useState(false); + const dragStartX = useRef(0); + const dragStartWidth = useRef(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( + minWidth, + Math.min(maxWidth, dragStartWidth.current + deltaX), + ); + setWidth(newWidth); + }, + [isDragging, minWidth, maxWidth], + ); + + 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, + }; +}