fix: improve integration tool interface

This commit is contained in:
Vukasin 2025-08-26 20:03:23 +02:00
parent f713c51daf
commit 39cd190311
3 changed files with 502 additions and 229 deletions

View File

@ -89,31 +89,31 @@ const getStepIndex = (step: Step): number => {
const StepIndicator = ({ currentStep, mode }: { currentStep: Step; mode: 'full' | 'profile-only' }) => {
const currentIndex = getStepIndex(currentStep);
const visibleSteps = mode === 'profile-only'
const visibleSteps = mode === 'profile-only'
? stepConfigs.filter(step => step.id !== Step.ToolsSelection && step.id !== Step.ProfileSelect)
: stepConfigs;
const visibleCurrentIndex = visibleSteps.findIndex(step => step.id === currentStep);
return (
<div className="px-8 py-6">
<div className="flex items-center justify-between relative">
<div className="absolute left-0 right-0 top-[14px] h-[2px] bg-muted-foreground/20 -z-10" />
<motion.div
<motion.div
className="absolute left-0 top-[14px] h-[2px] bg-primary -z-10"
initial={{ width: 0 }}
animate={{
width: `${(visibleCurrentIndex / (visibleSteps.length - 1)) * 100}%`
animate={{
width: `${(visibleCurrentIndex / (visibleSteps.length - 1)) * 100}%`
}}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
{visibleSteps.map((step, index) => {
const stepIndex = getStepIndex(step.id);
const isCompleted = stepIndex < currentIndex;
const isCurrent = step.id === currentStep;
const isUpcoming = stepIndex > currentIndex;
return (
<motion.div
key={step.id}
@ -195,7 +195,7 @@ const InitiationFieldInput = ({ field, value, onChange, error }: {
{field.required && <span className="text-destructive ml-1">*</span>}
</Label>
</div>
{isBooleanField ? (
<div className="flex items-center space-x-2">
<input
@ -220,7 +220,7 @@ const InitiationFieldInput = ({ field, value, onChange, error }: {
step={isNumberField ? "any" : undefined}
/>
)}
{field.description && !isBooleanField && (
<div className="flex items-start gap-2 text-xs text-muted-foreground">
<Info className="h-3 w-3 mt-0.5 flex-shrink-0" />
@ -247,13 +247,13 @@ const ToolPreviewCard = ({ tool, searchTerm }: {
if (!term) return text;
const regex = new RegExp(`(${term})`, 'gi');
const parts = text.split(regex);
return parts.map((part, index) =>
regex.test(part) ?
<mark key={index} className="bg-yellow-200 dark:bg-yellow-900 px-0.5">{part}</mark> :
return parts.map((part, index) =>
regex.test(part) ?
<mark key={index} className="bg-yellow-200 dark:bg-yellow-900 px-0.5">{part}</mark> :
part
);
};
// Generate icon based on tool name/category
const getToolIcon = (toolName: string) => {
const name = toolName.toLowerCase();
@ -282,9 +282,9 @@ const ToolPreviewCard = ({ tool, searchTerm }: {
</div>
);
};
return (
<div
<div
className="border rounded-md p-2 hover:bg-muted/30 transition-colors group cursor-pointer"
title={tool.description} // Show full description on hover
>
@ -322,7 +322,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
const [showToolsManager, setShowToolsManager] = useState(false);
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
const [selectedConnectionType, setSelectedConnectionType] = useState<'existing' | 'new' | null>(null);
const [initiationFields, setInitiationFields] = useState<Record<string, string>>({});
const [initiationFieldsErrors, setInitiationFieldsErrors] = useState<Record<string, string>>({});
@ -330,13 +330,13 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
const { mutate: createProfile, isPending: isCreating } = useCreateComposioProfile();
const { data: profiles, isLoading: isLoadingProfiles } = useComposioProfiles();
const { data: toolkitDetails, isLoading: isLoadingToolkitDetails } = useComposioToolkitDetails(
app.slug,
{ enabled: open && currentStep === Step.ProfileCreate }
);
const existingProfiles = profiles?.filter(p =>
const existingProfiles = profiles?.filter(p =>
p.toolkit_slug === app.slug && p.is_connected
) || [];
@ -344,13 +344,13 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
const [toolsPreviewSearchTerm, setToolsPreviewSearchTerm] = useState('');
const { data: toolsResponse, isLoading: isLoadingToolsPreview } = useComposioTools(
app.slug,
{
app.slug,
{
enabled: open && currentStep === Step.ProfileSelect,
limit: 50
limit: 50
}
);
const availableToolsPreview = toolsResponse?.tools || [];
useEffect(() => {
@ -382,38 +382,38 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
const validateInitiationFields = (): boolean => {
const newErrors: Record<string, string> = {};
const initiationRequirements = toolkitDetails?.toolkit.connected_account_initiation_fields;
if (initiationRequirements?.required) {
for (const field of initiationRequirements.required) {
if (field.required) {
const value = initiationFields[field.name];
const isEmpty = !value || value.trim() === '';
if (field.type.toLowerCase() === 'boolean') {
continue;
}
if ((field.type.toLowerCase() === 'number' || field.type.toLowerCase() === 'double') && value) {
if (isNaN(Number(value))) {
newErrors[field.name] = `${field.displayName} must be a valid number`;
continue;
}
}
if (isEmpty) {
newErrors[field.name] = `${field.displayName} is required`;
}
}
}
}
setInitiationFieldsErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSaveTools = async () => {
if (!selectedProfile || !agentId) return;
const mcpConfigResponse = await composioApi.getMcpConfigForProfile(selectedProfile.profile_id);
const response = await backendApi.put(`/agents/${agentId}/custom-mcp-tools`, {
custom_mcps: [{
@ -458,12 +458,12 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
toast.error('Profile name is required');
return;
}
if (!validateInitiationFields()) {
toast.error('Please fill in all required fields');
return;
}
createProfile({
toolkit_slug: app.slug,
profile_name: profileName,
@ -556,8 +556,8 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
}
};
const filteredToolsPreview = availableToolsPreview.filter(tool =>
!toolsPreviewSearchTerm ||
const filteredToolsPreview = availableToolsPreview.filter(tool =>
!toolsPreviewSearchTerm ||
tool.name.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()) ||
tool.tags?.some(tag => tag.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()))
@ -586,11 +586,11 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn(
"overflow-hidden gap-0",
currentStep === Step.ToolsSelection ? "max-w-2xl h-[85vh] p-0 flex flex-col" :
currentStep === Step.ProfileSelect ? "max-w-2xl p-0" : "max-w-lg p-0"
currentStep === Step.ToolsSelection ? "max-w-2xl h-[85vh] p-0 flex flex-col" :
currentStep === Step.ProfileSelect ? "max-w-2xl p-0" : "max-w-lg p-0"
)}>
<StepIndicator currentStep={currentStep} mode={mode} />
{currentStep !== Step.ToolsSelection ? (
<>
<DialogHeader className="px-8 pb-2">
@ -647,10 +647,10 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
{showToolsManager ? 'Hide' : 'View'} Tools
</Button>
</div>
<div className="grid gap-3">
{existingProfiles.length > 0 && (
<Card
<Card
className={cn(
"cursor-pointer p-0 transition-all",
selectedConnectionType === 'existing' ? "border-primary bg-primary/5" : "border-border hover:border-border/80"
@ -693,22 +693,22 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
<SelectValue placeholder="Select a profile..." />
</SelectTrigger>
<SelectContent>
{existingProfiles.map((profile) => (
<SelectItem key={profile.profile_id} value={profile.profile_id}>
<div className="flex items-center gap-3">
{app.logo ? (
<img src={app.logo} alt={app.name} className="w-5 h-5 rounded-lg object-contain bg-muted p-0.5 border flex-shrink-0" />
) : (
<div className="w-5 h-5 rounded-lg bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center text-primary text-xs font-semibold flex-shrink-0">
{app.name.charAt(0)}
{existingProfiles.map((profile) => (
<SelectItem key={profile.profile_id} value={profile.profile_id}>
<div className="flex items-center gap-3">
{app.logo ? (
<img src={app.logo} alt={app.name} className="w-5 h-5 rounded-lg object-contain bg-muted p-0.5 border flex-shrink-0" />
) : (
<div className="w-5 h-5 rounded-lg bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center text-primary text-xs font-semibold flex-shrink-0">
{app.name.charAt(0)}
</div>
)}
<div>
<div className="text-sm font-medium">{profile.profile_name}</div>
</div>
)}
<div>
<div className="text-sm font-medium">{profile.profile_name}</div>
</div>
</div>
</SelectItem>
))}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
@ -718,7 +718,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
</CardContent>
</Card>
)}
<Card className={cn(
"cursor-pointer p-0 transition-all",
selectedConnectionType === 'new' ? "border-primary bg-primary/5" : "border-border hover:border-border/80"
@ -748,51 +748,51 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
</Card>
</div>
</div>
<AnimatePresence>
{showToolsManager && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="space-y-3">
<ScrollArea className="h-[200px] border rounded-md bg-muted/10">
<div className="p-2">
{isLoadingToolsPreview ? (
<div className="grid grid-cols-2 gap-1">
{[...Array(8)].map((_, i) => (
<div key={i} className="border rounded-md p-2 animate-pulse bg-background/50">
<div className="h-2 bg-muted rounded w-3/4 mb-1"></div>
<div className="h-2 bg-muted rounded w-1/2"></div>
</div>
))}
</div>
) : filteredToolsPreview.length > 0 ? (
<div className="grid grid-cols-2 gap-1">
{filteredToolsPreview.map((tool) => (
<ToolPreviewCard
key={tool.slug}
tool={tool}
searchTerm={toolsPreviewSearchTerm}
/>
))}
</div>
) : (
<div className="text-center py-6 text-muted-foreground">
<Search className="h-4 w-4 mx-auto mb-2" />
<p className="text-xs">
{toolsPreviewSearchTerm ? 'No matches' : 'No tools available'}
</p>
</div>
)}
</div>
</ScrollArea>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showToolsManager && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="space-y-3">
<ScrollArea className="h-[200px] border rounded-md bg-muted/10">
<div className="p-2">
{isLoadingToolsPreview ? (
<div className="grid grid-cols-2 gap-1">
{[...Array(8)].map((_, i) => (
<div key={i} className="border rounded-md p-2 animate-pulse bg-background/50">
<div className="h-2 bg-muted rounded w-3/4 mb-1"></div>
<div className="h-2 bg-muted rounded w-1/2"></div>
</div>
))}
</div>
) : filteredToolsPreview.length > 0 ? (
<div className="grid grid-cols-2 gap-1">
{filteredToolsPreview.map((tool) => (
<ToolPreviewCard
key={tool.slug}
tool={tool}
searchTerm={toolsPreviewSearchTerm}
/>
))}
</div>
) : (
<div className="text-center py-6 text-muted-foreground">
<Search className="h-4 w-4 mx-auto mb-2" />
<p className="text-xs">
{toolsPreviewSearchTerm ? 'No matches' : 'No tools available'}
</p>
</div>
)}
</div>
</ScrollArea>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
@ -800,10 +800,10 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
<div className="px-6 py-4 border-t border-border/50 bg-muted/20 flex-shrink-0">
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{selectedConnectionType === 'new' ? 'Ready to create new connection' :
selectedConnectionType === 'existing' && selectedProfileId ? 'Profile selected' :
selectedConnectionType === 'existing' ? 'Select a profile to continue' :
'Choose how you want to connect'}
{selectedConnectionType === 'new' ? 'Ready to create new connection' :
selectedConnectionType === 'existing' && selectedProfileId ? 'Profile selected' :
selectedConnectionType === 'existing' ? 'Select a profile to continue' :
'Choose how you want to connect'}
</div>
<div className="flex gap-3">
<Button
@ -1021,9 +1021,9 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
<DialogHeader className="px-8 border-border/50 flex-shrink-0 bg-muted/10">
<div className="flex items-center gap-4">
{app.logo ? (
<img
src={app.logo}
alt={app.name}
<img
src={app.logo}
alt={app.name}
className="w-14 h-14 rounded-xl object-contain bg-muted p-2 border"
/>
) : (
@ -1058,6 +1058,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
profileId={selectedProfile.profile_id}
agentId={agentId}
toolkitName={selectedProfile.toolkit_name || app.name}
toolkitSlug={selectedProfile.toolkit_slug || app.slug}
selectedTools={selectedTools}
onToolsChange={setSelectedTools}
onSave={handleSaveTools}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
@ -46,9 +46,40 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
enabled: !!currentProfile?.toolkit_slug
});
// Load current tools when dialog opens
useEffect(() => {
const loadCurrentTools = async () => {
if (!open || !agentId || !profileId) return;
try {
const response = await backendApi.get(`/agents/${agentId}`);
if (response.success && response.data) {
const agent = response.data;
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
mcp.type === 'composio' && mcp.config?.profile_id === profileId
) || [];
const enabledTools = composioMcps.flatMap((mcp: any) => mcp.enabledTools || []);
console.log('Loading current tools for editing:', enabledTools);
setSelectedTools(enabledTools);
}
} catch (error) {
console.error('Failed to load current tools:', error);
setSelectedTools([]);
}
};
if (open) {
loadCurrentTools();
} else {
// Reset when dialog closes
setSelectedTools([]);
}
}, [open, agentId, profileId]);
const handleSave = async () => {
if (!currentProfile) return;
const mcpConfigResponse = await composioApi.getMcpConfigForProfile(currentProfile.profile_id);
const response = await backendApi.put(`/agents/${agentId}/custom-mcp-tools`, {
custom_mcps: [{
@ -71,9 +102,9 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
<DialogHeader className="p-6 pb-4 border-b flex-shrink-0">
<div className="flex items-center gap-3">
{iconData?.icon_url || appLogo ? (
<img
src={iconData?.icon_url || appLogo}
alt={currentProfile?.toolkit_name}
<img
src={iconData?.icon_url || appLogo}
alt={currentProfile?.toolkit_name}
className="w-10 h-10 rounded-lg border object-contain bg-muted p-1"
/>
) : (
@ -91,11 +122,12 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
</div>
</div>
</DialogHeader>
<ComposioToolsSelector
profileId={currentProfile.profile_id}
agentId={agentId}
toolkitName={currentProfile.toolkit_name}
toolkitSlug={currentProfile.toolkit_slug}
selectedTools={selectedTools}
onToolsChange={setSelectedTools}
onSave={handleSave}

View File

@ -1,28 +1,32 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Search, Save, AlertCircle, Loader2 } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Search, Save, AlertCircle, Loader2, Filter, X, ChevronDown } from 'lucide-react';
import { backendApi } from '@/lib/api-client';
import { composioApi } from '@/hooks/react-query/composio/utils';
import { useComposioTools } from '@/hooks/react-query/composio/use-composio';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
export interface Tool {
name: string;
description: string;
parameters: any;
}
import type { ComposioTool } from '@/hooks/react-query/composio/utils';
interface ComposioToolsSelectorProps {
profileId: string;
profileId?: string;
agentId?: string;
toolkitName: string;
toolkitSlug: string;
selectedTools: string[];
onToolsChange: (tools: string[]) => void;
onSave?: () => Promise<void>;
@ -33,7 +37,7 @@ interface ComposioToolsSelectorProps {
}
const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
tool: Tool;
tool: ComposioTool;
isSelected: boolean;
onToggle: () => void;
searchTerm: string;
@ -42,7 +46,7 @@ const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
if (!term) return text;
const regex = new RegExp(`(${term})`, 'gi');
const parts = text.split(regex);
return parts.map((part, i) =>
return parts.map((part, i) =>
regex.test(part) ? <mark key={i} className="bg-yellow-200 dark:bg-yellow-900/50">{part}</mark> : part
);
};
@ -55,30 +59,39 @@ const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
<CardContent className="p-4" onClick={onToggle}>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-medium text-sm truncate">
{highlightText(tool.name, searchTerm)}
</h3>
{tool.tags?.includes('important') && (
<Badge variant="secondary" className="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
Important
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
{highlightText(tool.description || 'No description available', searchTerm)}
</p>
{tool.parameters?.properties && (
<div className="mt-2">
<div className="flex items-center gap-2 flex-wrap">
{tool.input_parameters?.properties && (
<Badge variant="outline" className="text-xs">
{Object.keys(tool.parameters.properties).length} parameters
{Object.keys(tool.input_parameters.properties).length} params
</Badge>
</div>
)}
)}
{tool.tags?.filter(tag => tag !== 'important').slice(0, 2).map(tag => (
<Badge key={tag} variant="outline" className="text-xs text-muted-foreground">
{tag}
</Badge>
))}
</div>
</div>
<div className="flex-shrink-0 ml-2">
<Switch
<Checkbox
checked={isSelected}
onCheckedChange={onToggle}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
@ -106,6 +119,7 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
profileId,
agentId,
toolkitName,
toolkitSlug,
selectedTools,
onToolsChange,
onSave,
@ -115,103 +129,223 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
searchPlaceholder = "Search tools..."
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [availableTools, setAvailableTools] = useState<Tool[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedTagFilters, setSelectedTagFilters] = useState<Set<string>>(new Set());
const initializedRef = useRef(false);
const filteredTools = useMemo(() => {
if (!searchTerm) return availableTools;
const term = searchTerm.toLowerCase();
return availableTools.filter(tool =>
tool.name.toLowerCase().includes(term) ||
(tool.description && tool.description.toLowerCase().includes(term))
);
}, [availableTools, searchTerm]);
const { data: toolsResponse, isLoading, error } = useComposioTools(toolkitSlug, { enabled: !!toolkitSlug });
useEffect(() => {
if (profileId) {
console.log('ComposioToolsSelector: Loading tools for profile', profileId);
loadTools();
if (agentId) {
loadCurrentAgentTools();
}
}
}, [profileId, agentId]);
// DEDUPE: Remove duplicate tools by name, prioritizing ones with "important" tag
const availableTools = useMemo(() => {
const rawTools = toolsResponse?.tools || [];
const toolMap = new Map();
const loadTools = async () => {
setIsLoading(true);
setError(null);
try {
console.log('ComposioToolsSelector: Discovering tools for profile', profileId);
const response = await composioApi.discoverTools(profileId);
console.log('ComposioToolsSelector: Tools response', response);
if (response.success && response.tools) {
setAvailableTools(response.tools);
console.log('ComposioToolsSelector: Loaded', response.tools.length, 'tools');
// First pass: add all tools
rawTools.forEach(tool => {
const existing = toolMap.get(tool.name);
if (!existing) {
toolMap.set(tool.name, tool);
} else {
setError('Failed to load available tools');
console.error('ComposioToolsSelector: Failed to load tools', response);
// If we have a duplicate, prefer the one with "important" tag
const hasImportant = tool.tags?.includes('important');
const existingHasImportant = existing.tags?.includes('important');
if (hasImportant && !existingHasImportant) {
toolMap.set(tool.name, tool);
}
}
} catch (err: any) {
setError(err.message || 'Failed to load tools');
console.error('ComposioToolsSelector: Error loading tools', err);
} finally {
setIsLoading(false);
}
};
});
return Array.from(toolMap.values());
}, [toolsResponse?.tools]);
const loadCurrentAgentTools = async () => {
if (!agentId) return;
if (!agentId || !profileId) {
console.log('Missing agentId or profileId:', { agentId, profileId });
return;
}
console.log('Loading tools for agent:', { agentId, profileId });
try {
console.log('ComposioToolsSelector: Loading current agent tools for agent', agentId, 'profile', profileId);
const response = await backendApi.get(`/agents/${agentId}`);
console.log('Agent response:', response);
if (response.success && response.data) {
const agent = response.data;
console.log('ComposioToolsSelector: Agent data', agent);
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
mcp.type === 'composio' && mcp.config?.profile_id === profileId
) || [];
console.log('Found composio MCPs:', composioMcps);
const enabledTools = composioMcps.flatMap((mcp: any) => mcp.enabledTools || []);
console.log('ComposioToolsSelector: Found enabled tools', enabledTools);
onToolsChange(enabledTools);
console.log('Enabled tools from agent:', enabledTools);
// If no existing tools found, auto-select important tools
if (enabledTools.length === 0 && availableTools.length > 0) {
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
const importantToolNames = importantTools.map(tool => tool.name);
if (importantToolNames.length > 0) {
console.log('No existing tools, selecting important tools:', importantToolNames);
onToolsChange(importantToolNames);
} else {
console.log('No existing tools and no important tools, selecting none');
onToolsChange([]);
}
} else {
console.log('Setting tools from agent:', enabledTools);
onToolsChange(enabledTools);
}
initializedRef.current = true;
}
} catch (err) {
console.error('ComposioToolsSelector: Failed to load current agent tools:', err);
console.error('Failed to load current agent tools:', err);
// If API call fails, fall back to important tools
if (availableTools.length > 0) {
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
const importantToolNames = importantTools.map(tool => tool.name);
if (importantToolNames.length > 0) {
onToolsChange(importantToolNames);
} else {
onToolsChange([]);
}
}
initializedRef.current = true;
}
};
// INSTANT: Show important tools as selected the moment they appear
const getEffectiveSelectedTools = useMemo(() => {
// Always use explicit selections if they exist
if (selectedTools.length > 0) {
return selectedTools;
}
// Only auto-select important tools for NEW agents (no agentId) when no tools are selected
if (!agentId && availableTools.length > 0) {
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
const importantToolNames = importantTools.map(tool => tool.name);
console.log('Auto-selecting important tools for new agent:', {
totalTools: availableTools.length,
importantTools: importantTools.length,
importantToolNames,
});
return importantToolNames;
}
// For existing agents, return empty if no tools are explicitly selected
// (they should be loaded via loadCurrentAgentTools)
return selectedTools;
}, [selectedTools, availableTools, agentId]);
// Get all unique tags
const allTags = useMemo(() => {
const tags = new Set<string>();
availableTools.forEach(tool => {
tool.tags?.forEach(tag => tags.add(tag));
});
return Array.from(tags).sort((a, b) => {
// Put "important" first, then alphabetical
if (a === 'important') return -1;
if (b === 'important') return 1;
return a.localeCompare(b);
});
}, [availableTools]);
// Sync the effective selection back to parent when tools load
useEffect(() => {
console.log('useEffect triggered:', {
initialized: initializedRef.current,
availableToolsCount: availableTools.length,
agentId,
profileId,
selectedToolsCount: selectedTools.length
});
if (!initializedRef.current && availableTools.length > 0) {
initializedRef.current = true;
if (agentId && profileId) {
// For existing agents, load their tools
console.log('Loading tools for existing agent');
loadCurrentAgentTools();
} else if (selectedTools.length === 0) {
// For new agents, sync the important tools selection
console.log('Setting up new agent with important tools');
const importantToolNames = availableTools
.filter(tool => tool.tags?.includes('important'))
.map(tool => tool.name);
if (importantToolNames.length > 0) {
onToolsChange(importantToolNames);
}
}
}
}, [availableTools, agentId, profileId]); // Added agentId and profileId as dependencies
// Filter tools based on selected tag filters and search
const filteredTools = useMemo(() => {
let tools = availableTools;
// Filter by tags if any are selected
if (selectedTagFilters.size > 0) {
tools = tools.filter(tool =>
tool.tags?.some(tag => selectedTagFilters.has(tag)) ||
(selectedTagFilters.has('untagged') && (!tool.tags || tool.tags.length === 0))
);
}
// Filter by search term
if (searchTerm) {
const term = searchTerm.toLowerCase();
tools = tools.filter(tool =>
tool.name.toLowerCase().includes(term) ||
tool.description.toLowerCase().includes(term)
);
}
return tools;
}, [availableTools, selectedTagFilters, searchTerm]);
const handleToolToggle = (toolName: string) => {
const newTools = selectedTools.includes(toolName)
? selectedTools.filter(t => t !== toolName)
: [...selectedTools, toolName];
const effectiveSelected = getEffectiveSelectedTools;
const newTools = effectiveSelected.includes(toolName)
? effectiveSelected.filter(t => t !== toolName)
: [...effectiveSelected, toolName];
onToolsChange(newTools);
};
const handleSelectAll = () => {
const allToolNames = filteredTools.map(tool => tool.name);
const hasAll = allToolNames.every(name => selectedTools.includes(name));
if (hasAll) {
const newTools = selectedTools.filter(name => !allToolNames.includes(name));
onToolsChange(newTools);
onToolsChange(allToolNames);
};
const handleSelectNone = () => {
onToolsChange([]);
};
const handleTagFilterToggle = (tag: string) => {
const newFilters = new Set(selectedTagFilters);
if (newFilters.has(tag)) {
// Remove tag filter - just affects visibility
newFilters.delete(tag);
} else {
const newTools = [...selectedTools];
allToolNames.forEach(name => {
if (!newTools.includes(name)) {
newTools.push(name);
}
});
onToolsChange(newTools);
// Add tag filter - just affects visibility
newFilters.add(tag);
}
setSelectedTagFilters(newFilters);
};
const handleSave = async () => {
if (!agentId || !onSave) return;
try {
setIsSaving(true);
await onSave();
@ -224,14 +358,13 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
};
const selectedCount = selectedTools.length;
const filteredSelectedCount = filteredTools.filter(tool => selectedTools.includes(tool.name)).length;
const allFilteredSelected = filteredTools.length > 0 && filteredSelectedCount === filteredTools.length;
const totalTools = availableTools.length;
return (
<div className={cn("flex flex-col h-full", className)}>
{/* Search and Controls Bar */}
<div className="px-6 py-3 border-b bg-muted/20 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 mb-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
@ -241,30 +374,122 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
className="pl-10 h-9 bg-background border-0 focus-visible:ring-1"
/>
</div>
<div className="flex items-center gap-3 text-sm">
<span className="text-muted-foreground whitespace-nowrap">
{filteredTools.length} {searchTerm && `of ${availableTools.length}`} tools
</span>
{selectedCount > 0 && (
<Badge variant="secondary" className="bg-primary/10 text-primary border-0">
{selectedCount}
</Badge>
)}
{filteredTools.length > 0 && (
{/* Tag Filter Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
className="h-8 text-xs"
className={cn(
"h-9 gap-2 shadow-none",
selectedTagFilters.size > 0 && "border-primary/50 bg-primary/5"
)}
>
{allFilteredSelected ? 'Deselect' : 'Select'} All
<Filter className="h-4 w-4" />
<span>Filter
<span className="text-muted-foreground"> ({selectedTagFilters.size || 0})</span>
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-80"
align="start"
side="bottom"
sideOffset={4}
>
<DropdownMenuLabel className="flex items-center justify-between">
<span>Filter by tags</span>
{selectedTagFilters.size > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedTagFilters(new Set())}
className="h-6 px-2 text-xs text-muted-foreground hover:text-destructive"
>
<X className="h-3 w-3 mr-1" />
Clear
</Button>
)}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="max-h-56 overflow-y-auto">
{allTags.map(tag => {
const isSelected = selectedTagFilters.has(tag);
const toolsWithTag = availableTools.filter(tool => tool.tags?.includes(tag));
return (
<DropdownMenuItem
key={tag}
className="flex items-center space-x-3 cursor-pointer focus:bg-accent"
onClick={(e) => {
e.preventDefault();
handleTagFilterToggle(tag);
}}
onSelect={(e) => {
e.preventDefault();
}}
>
<Checkbox
checked={isSelected}
onChange={() => handleTagFilterToggle(tag)}
className="pointer-events-none"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className={cn(
"text-sm font-medium truncate",
tag === 'important' && "text-orange-600"
)}>
{tag}
</span>
<Badge variant="outline" className="text-xs ml-2 shrink-0">
{toolsWithTag.length}
</Badge>
</div>
</div>
</DropdownMenuItem>
);
})}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Compact Filter + Quick Actions */}
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground whitespace-nowrap text-sm">
Showing {filteredTools.length} of {availableTools.length} tools
</span>
<div className='flex items-center gap-2'>
<div className="h-4 w-px bg-border" />
{/* Quick Actions */}
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
disabled={isLoading || filteredTools.length === 0}
className="h-8 text-xs"
>
Select All
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleSelectNone}
disabled={isLoading}
className="h-8 text-xs"
>
Clear
</Button>
</div>
</div>
</div>
{/* Tools List */}
@ -273,23 +498,23 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
{error && (
<Alert className="mb-6">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
<AlertDescription>{error.message || 'Failed to load tools'}</AlertDescription>
</Alert>
)}
{isLoading ? (
<div className="space-y-3">
<div className="grid gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<ToolSkeleton key={i} />
))}
</div>
) : filteredTools.length > 0 ? (
<div className="space-y-3">
<div className="grid gap-3">
{filteredTools.map((tool) => (
<ToolCard
key={tool.name}
key={tool.slug}
tool={tool}
isSelected={selectedTools.includes(tool.name)}
isSelected={getEffectiveSelectedTools.includes(tool.name)}
onToggle={() => handleToolToggle(tool.name)}
searchTerm={searchTerm}
/>
@ -300,9 +525,24 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
<div className="w-12 h-12 rounded-xl bg-muted/50 flex items-center justify-center mx-auto mb-4">
<Search className="h-6 w-6 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground">
{searchTerm ? `No tools found matching "${searchTerm}"` : 'No tools available'}
<p className="text-sm text-muted-foreground mb-2">
{searchTerm
? `No tools found matching "${searchTerm}"`
: selectedTagFilters.size > 0
? 'No tools match the selected filters'
: 'No tools available'
}
</p>
{selectedTagFilters.size > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => setSelectedTagFilters(new Set())}
className="mt-2"
>
Clear all filters
</Button>
)}
</div>
)}
</div>
@ -313,8 +553,8 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
<div className="p-6 pt-4 border-t bg-muted/20 flex-shrink-0">
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{selectedCount > 0 ? (
`${selectedCount} tool${selectedCount === 1 ? '' : 's'} will be added to your agent`
{getEffectiveSelectedTools.length > 0 ? (
`${getEffectiveSelectedTools.length} tool${getEffectiveSelectedTools.length === 1 ? '' : 's'} will be added to your agent`
) : (
'No tools selected'
)}