Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { MCPToolType } from "@mastra/core/mcp";
import { MastraMCPServerDefinition } from "@/shared/types.js";
import { ElicitationDialog } from "./ElicitationDialog";
import { TruncatedText } from "@/components/ui/truncated-text";
import { SearchInput } from "@/components/ui/search-input";

interface Tool {
name: string;
Expand Down Expand Up @@ -70,6 +71,7 @@ export function ToolsTab({ serverConfig }: ToolsTabProps) {
const [elicitationRequest, setElicitationRequest] =
useState<ElicitationRequest | null>(null);
const [elicitationLoading, setElicitationLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");

useEffect(() => {
if (serverConfig) {
Expand Down Expand Up @@ -502,6 +504,13 @@ export function ToolsTab({ serverConfig }: ToolsTabProps) {
};

const toolNames = Object.keys(tools);
const filteredToolNames = searchQuery.trim()
? toolNames.filter((name) => {
const tool = tools[name];
const haystack = `${name} ${tool?.description || ""}`.toLowerCase();
return haystack.includes(searchQuery.trim().toLowerCase());
})
: toolNames;

if (!serverConfig) {
return (
Expand All @@ -525,26 +534,33 @@ export function ToolsTab({ serverConfig }: ToolsTabProps) {
<ResizablePanel defaultSize={30} minSize={20} maxSize={50}>
<div className="h-full flex flex-col border-r border-border bg-background">
{/* Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-border bg-background">
<div className="flex items-center gap-3">
<Wrench className="h-3 w-3 text-muted-foreground" />
<h2 className="text-xs font-semibold text-foreground">
Tools
</h2>
<Badge variant="secondary" className="text-xs font-mono">
{toolNames.length}
</Badge>
<div className="px-4 py-4 border-b border-border bg-background space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Wrench className="h-3 w-3 text-muted-foreground" />
<h2 className="text-xs font-semibold text-foreground">
Tools
</h2>
<Badge variant="secondary" className="text-xs font-mono">
{toolNames.length}
</Badge>
</div>
<Button
onClick={fetchTools}
variant="ghost"
size="sm"
disabled={fetchingTools}
>
<RefreshCw
className={`h-3 w-3 ${fetchingTools ? "animate-spin" : ""} cursor-pointer`}
/>
</Button>
</div>
<Button
onClick={fetchTools}
variant="ghost"
size="sm"
disabled={fetchingTools}
>
<RefreshCw
className={`h-3 w-3 ${fetchingTools ? "animate-spin" : ""} cursor-pointer`}
/>
</Button>
<SearchInput
value={searchQuery}
onValueChange={setSearchQuery}
placeholder="Search tools by name or description"
/>
</div>

{/* Tools List */}
Expand All @@ -563,15 +579,17 @@ export function ToolsTab({ serverConfig }: ToolsTabProps) {
Fetching available tools from server
</p>
</div>
) : toolNames.length === 0 ? (
) : filteredToolNames.length === 0 ? (
<div className="text-center py-8">
<p className="text-sm text-muted-foreground">
No tools available
{toolNames.length === 0
? "No tools available"
: "No tools match your search"}
</p>
</div>
) : (
<div className="space-y-1">
{toolNames.map((name) => (
{filteredToolNames.map((name) => (
<div
key={name}
className={`cursor-pointer transition-all duration-200 hover:bg-muted/30 dark:hover:bg-muted/50 p-3 rounded-md mx-2 ${
Expand Down
68 changes: 68 additions & 0 deletions client/src/components/ui/search-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from "react";
import { Input } from "@/components/ui/input";
import { Search, X } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SearchInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
value: string;
onValueChange: (value: string) => void;
clearable?: boolean;
iconClassName?: string;
}

export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
(
{
value,
onValueChange,
placeholder = "Search…",
className,
clearable = true,
iconClassName,
...props
},
ref,
) => {
return (
<div className="relative">
<Search
className={cn(
"absolute left-2.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground",
iconClassName,
)}
/>
<Input
ref={ref}
value={value}
onChange={(e) => onValueChange(e.target.value)}
placeholder={placeholder}
type="search"
role="searchbox"
aria-label={placeholder}
autoComplete="off"
spellCheck={false}
className={cn(
"pl-7 h-8 bg-background border-border hover:border-border/80 text-xs md:text-xs leading-4 text-foreground placeholder:text-muted-foreground/70",
className,
)}
{...props}
/>
{clearable && value && (
<button
type="button"
onClick={() => onValueChange("")}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear search"
>
<X className="h-3 w-3" />
</button>
)}
</div>
);
},
);

SearchInput.displayName = "SearchInput";

export default SearchInput;