Successfully Connected!
Your {app.name} integration is ready.
diff --git a/frontend/src/components/agents/composio/composio-registry.tsx b/frontend/src/components/agents/composio/composio-registry.tsx
index 0a16dcbb..0184eab1 100644
--- a/frontend/src/components/agents/composio/composio-registry.tsx
+++ b/frontend/src/components/agents/composio/composio-registry.tsx
@@ -4,16 +4,19 @@ import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
-import { Search, Zap, X } from 'lucide-react';
+import { Search, Zap, X, Settings, ChevronDown, ChevronUp } from 'lucide-react';
import { useComposioToolkits, useComposioCategories } from '@/hooks/react-query/composio/use-composio';
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
-import { useAgent } from '@/hooks/react-query/agents/use-agents';
+import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
import { ComposioConnector } from './composio-connector';
+import { ComposioToolsManager } from './composio-tools-manager';
import type { ComposioToolkit, ComposioProfile } from '@/hooks/react-query/composio/utils';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { useQueryClient } from '@tanstack/react-query';
import { AgentSelector } from '../../thread/chat-input/agent-selector';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+import { Switch } from '@/components/ui/switch';
const CATEGORY_EMOJIS: Record = {
'popular': '🔥',
@@ -26,6 +29,16 @@ const CATEGORY_EMOJIS: Record = {
'scheduling': '📅',
};
+interface ConnectedApp {
+ toolkit: ComposioToolkit;
+ profile: ComposioProfile;
+ mcpConfig: {
+ name: string;
+ type: string;
+ config: Record;
+ enabledTools: string[];
+ };
+}
interface ComposioRegistryProps {
onToolsSelected?: (profileId: string, selectedTools: string[], appName: string, appSlug: string) => void;
@@ -37,6 +50,52 @@ interface ComposioRegistryProps {
onAgentChange?: (agentId: string | undefined) => void;
}
+// Helper function to get agent-specific connected apps
+const getAgentConnectedApps = (
+ agent: any,
+ profiles: ComposioProfile[],
+ toolkits: ComposioToolkit[]
+): ConnectedApp[] => {
+ if (!agent?.custom_mcps || !profiles?.length || !toolkits?.length) return [];
+
+ const connectedApps: ConnectedApp[] = [];
+
+ agent.custom_mcps.forEach((mcpConfig: any) => {
+ // Check if this is a Composio MCP by looking for profile_id in config
+ if (mcpConfig.config?.profile_id) {
+ const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id);
+ const toolkit = toolkits.find(t => t.slug === profile?.toolkit_slug);
+
+ if (profile && toolkit) {
+ connectedApps.push({
+ toolkit,
+ profile,
+ mcpConfig
+ });
+ }
+ }
+ });
+
+ return connectedApps;
+};
+
+// Helper function to check if an app is connected to the agent
+const isAppConnectedToAgent = (
+ agent: any,
+ appSlug: string,
+ profiles: ComposioProfile[]
+): boolean => {
+ if (!agent?.custom_mcps) return false;
+
+ return agent.custom_mcps.some((mcpConfig: any) => {
+ if (mcpConfig.config?.profile_id) {
+ const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id);
+ return profile?.toolkit_slug === appSlug;
+ }
+ return false;
+ });
+};
+
const AppCardSkeleton = () => (
@@ -57,16 +116,100 @@ const AppCardSkeleton = () => (
);
-const AppCard = ({ app, profiles, onConnect, onConfigure }: {
+const ConnectedAppSkeleton = () => (
+
+);
+
+const ConnectedAppCard = ({
+ connectedApp,
+ onToggleTools,
+ onConfigure,
+ onManageTools,
+ isUpdating
+}: {
+ connectedApp: ConnectedApp;
+ onToggleTools: (profileId: string, enabled: boolean) => void;
+ onConfigure: (app: ComposioToolkit, profile: ComposioProfile) => void;
+ onManageTools: (connectedApp: ConnectedApp) => void;
+ isUpdating: boolean;
+}) => {
+ const { toolkit, profile, mcpConfig } = connectedApp;
+ const hasEnabledTools = mcpConfig.enabledTools && mcpConfig.enabledTools.length > 0;
+
+ return (
+
+
+ {toolkit.logo ? (
+

+ ) : (
+
+ {toolkit.name.charAt(0)}
+
+ )}
+
+
{toolkit.name}
+
+ Connected as "{profile.profile_name}"
+
+
+
+
+
+
+
+
+
+
+
+ {hasEnabledTools ? `${mcpConfig.enabledTools.length} tools enabled` : 'Connected (no tools)'}
+
+
+
+
+ );
+};
+
+const AppCard = ({ app, profiles, onConnect, onConfigure, isConnectedToAgent, currentAgentId, mode }: {
app: ComposioToolkit;
profiles: ComposioProfile[];
onConnect: () => void;
onConfigure: (profile: ComposioProfile) => void;
+ isConnectedToAgent: boolean;
+ currentAgentId?: string;
+ mode?: 'full' | 'profile-only';
}) => {
const connectedProfiles = profiles.filter(p => p.is_connected);
+ const canConnect = mode === 'profile-only' ? true : (!isConnectedToAgent && currentAgentId);
return (
-
0 ? () => onConfigure(connectedProfiles[0]) : onConnect} className="group border bg-card hover:bg-muted rounded-2xl p-4 transition-all duration-200">
+
0 ? () => onConfigure(connectedProfiles[0]) : onConnect) : undefined}
+ className={cn(
+ "group border bg-card rounded-2xl p-4 transition-all duration-200",
+ canConnect ? "hover:bg-muted cursor-pointer" : "opacity-60 cursor-not-allowed"
+ )}
+ >
{app.logo ? (

@@ -99,11 +242,32 @@ const AppCard = ({ app, profiles, onConnect, onConfigure }: {
)}
- {connectedProfiles.length > 0 && (
+ {mode === 'profile-only' ? (
+
+
+
+ {connectedProfiles.length > 0 ? `${connectedProfiles.length} existing profile${connectedProfiles.length !== 1 ? 's' : ''}` : 'Click to connect'}
+
+
+ ) : isConnectedToAgent ? (
+
+
+
+ Connected to this agent
+
+
+ ) : connectedProfiles.length > 0 ? (
- Connected ({connectedProfiles.length})
+ Profile available ({connectedProfiles.length})
+
+
+ ) : (
+
)}
@@ -125,16 +289,20 @@ export const ComposioRegistry: React.FC
= ({
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedApp, setSelectedApp] = useState(null);
const [showConnector, setShowConnector] = useState(false);
+ const [showConnectedApps, setShowConnectedApps] = useState(true);
+ const [showToolsManager, setShowToolsManager] = useState(false);
+ const [selectedConnectedApp, setSelectedConnectedApp] = useState(null);
const [internalSelectedAgentId, setInternalSelectedAgentId] = useState(selectedAgentId);
const queryClient = useQueryClient();
const { data: categoriesData, isLoading: isLoadingCategories } = useComposioCategories();
const { data: toolkits, isLoading } = useComposioToolkits(search, selectedCategory);
- const { data: profiles } = useComposioProfiles();
+ const { data: profiles, isLoading: isLoadingProfiles } = useComposioProfiles();
const currentAgentId = selectedAgentId ?? internalSelectedAgentId;
- const { data: agent } = useAgent(currentAgentId || '');
+ const { data: agent, isLoading: isLoadingAgent } = useAgent(currentAgentId || '');
+ const { mutate: updateAgent, isPending: isUpdatingAgent } = useUpdateAgent();
const handleAgentSelect = (agentId: string | undefined) => {
if (onAgentChange) {
@@ -157,13 +325,20 @@ export const ComposioRegistry: React.FC = ({
return grouped;
}, [profiles]);
+ const connectedApps = useMemo(() => {
+ if (!currentAgentId || !agent) return [];
+ return getAgentConnectedApps(agent, profiles || [], toolkits?.toolkits || []);
+ }, [agent, profiles, toolkits, currentAgentId]);
+
+ const isLoadingConnectedApps = currentAgentId && (isLoadingAgent || isLoadingProfiles || isLoading);
+
const filteredToolkits = useMemo(() => {
if (!toolkits?.toolkits) return [];
return toolkits.toolkits;
}, [toolkits]);
const handleConnect = (app: ComposioToolkit) => {
- if (!currentAgentId && showAgentSelector) {
+ if (mode !== 'profile-only' && !currentAgentId && showAgentSelector) {
toast.error('Please select an agent first');
return;
}
@@ -172,7 +347,7 @@ export const ComposioRegistry: React.FC = ({
};
const handleConfigure = (app: ComposioToolkit, profile: ComposioProfile) => {
- if (!currentAgentId) {
+ if (mode !== 'profile-only' && !currentAgentId) {
toast.error('Please select an agent first');
return;
}
@@ -180,6 +355,37 @@ export const ComposioRegistry: React.FC = ({
setShowConnector(true);
};
+ const handleToggleTools = (profileId: string, enabled: boolean) => {
+ if (!currentAgentId || !agent) return;
+
+ const updatedCustomMcps = agent.custom_mcps?.map((mcpConfig: any) => {
+ if (mcpConfig.config?.profile_id === profileId) {
+ return {
+ ...mcpConfig,
+ enabledTools: enabled ? mcpConfig.enabledTools || [] : []
+ };
+ }
+ return mcpConfig;
+ }) || [];
+
+ updateAgent({
+ agentId: currentAgentId,
+ custom_mcps: updatedCustomMcps
+ }, {
+ onSuccess: () => {
+ toast.success(enabled ? 'Tools enabled' : 'Tools disabled');
+ },
+ onError: (error: any) => {
+ toast.error(error.message || 'Failed to update tools');
+ }
+ });
+ };
+
+ const handleManageTools = (connectedApp: ConnectedApp) => {
+ setSelectedConnectedApp(connectedApp);
+ setShowToolsManager(true);
+ };
+
const handleConnectionComplete = (profileId: string, appName: string, appSlug: string) => {
setShowConnector(false);
queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] });
@@ -251,9 +457,14 @@ export const ComposioRegistry: React.FC = ({
-
App Integrations
+
+ {mode === 'profile-only' ? 'Connect New App' : 'App Integrations'}
+
- Connect your favorite apps powered by Composio
+ {mode === 'profile-only'
+ ? 'Create a connection profile for your favorite apps'
+ : `Connect your favorite apps with ${currentAgentId ? 'this agent' : 'your agent'}`
+ }
@@ -264,11 +475,6 @@ export const ComposioRegistry: React.FC = ({
isSunaAgent={agent?.metadata?.is_suna_default}
/>
)}
- {onClose && (
-
- )}
@@ -301,42 +507,103 @@ export const ComposioRegistry: React.FC
= ({
-
- {isLoading ? (
-
- {Array.from({ length: 12 }).map((_, i) => (
-
- ))}
-
- ) : filteredToolkits.length === 0 ? (
-
-
-
-
-
No apps found
-
- {search ? `No apps match "${search}"` : 'No apps available in this category'}
-
-
- ) : (
-
- {filteredToolkits.map((app) => (
-
handleConnect(app)}
- onConfigure={(profile) => handleConfigure(app, profile)}
- />
- ))}
-
+
+ {currentAgentId && (
+
+
+
+
+
Connected to this agent
+ {isLoadingConnectedApps ? (
+
+ ) : connectedApps.length > 0 && (
+
+ {connectedApps.length}
+
+ )}
+
+ {showConnectedApps ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isLoadingConnectedApps ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : connectedApps.length === 0 ? (
+
+
+
+
+
No connected apps
+
Connect apps below to manage tools for this agent.
+
+ ) : (
+
+ {connectedApps.map((connectedApp) => (
+
+ ))}
+
+ )}
+
+
)}
+
+
+ {currentAgentId ? 'Available Apps' : 'Browse Apps'}
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 12 }).map((_, i) => (
+
+ ))}
+
+ ) : filteredToolkits.length === 0 ? (
+
+
+
+
+
No apps found
+
+ {search ? `No apps match "${search}"` : 'No apps available in this category'}
+
+
+ ) : (
+
+ {filteredToolkits.map((app) => (
+
handleConnect(app)}
+ onConfigure={(profile) => handleConfigure(app, profile)}
+ isConnectedToAgent={isAppConnectedToAgent(agent, app.slug, profiles || [])}
+ currentAgentId={currentAgentId}
+ mode={mode}
+ />
+ ))}
+
+ )}
+
-
{selectedApp && (
= ({
open={showConnector}
onOpenChange={setShowConnector}
onComplete={handleConnectionComplete}
+ mode={mode}
+ />
+ )}
+
+ {selectedConnectedApp && currentAgentId && (
+ {
+ queryClient.invalidateQueries({ queryKey: ['agents', currentAgentId] });
+ }}
/>
)}
diff --git a/frontend/src/components/agents/composio/composio-tools-manager.tsx b/frontend/src/components/agents/composio/composio-tools-manager.tsx
index 34370c8e..14150db6 100644
--- a/frontend/src/components/agents/composio/composio-tools-manager.tsx
+++ b/frontend/src/components/agents/composio/composio-tools-manager.tsx
@@ -11,6 +11,7 @@ import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Search, Save, AlertCircle, Zap, CheckCircle2, Settings2, Loader2 } from 'lucide-react';
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
+import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
import { backendApi } from '@/lib/api-client';
import { toast } from 'sonner';
import { useQueryClient } from '@tanstack/react-query';
@@ -129,6 +130,9 @@ export const ComposioToolsManager: React.FC
= ({
const { data: profiles } = useComposioProfiles();
const currentProfile = profileInfo || profiles?.find(p => p.profile_id === profileId);
+ const { data: iconData } = useComposioToolkitIcon(currentProfile?.toolkit_slug || '', {
+ enabled: !!currentProfile?.toolkit_slug
+ });
const filteredTools = useMemo(() => {
if (!searchTerm) return availableTools;
@@ -241,11 +245,11 @@ export const ComposioToolsManager: React.FC = ({
- {appLogo ? (
+ {iconData?.icon_url || appLogo ? (

) : (