refactor tools UX: part 1

This commit is contained in:
arkml 2025-07-16 20:13:03 +05:30 committed by Ramnique Singh
parent cccd383b92
commit 751a86c34d
21 changed files with 1462 additions and 258 deletions

View file

@ -3,6 +3,9 @@ import { z } from "zod";
import { import {
listToolkits as libListToolkits, listToolkits as libListToolkits,
listTools as libListTools, listTools as libListTools,
searchTools as libSearchTools,
getToolsByIds as libGetToolsByIds,
getTool as libGetTool,
getConnectedAccount as libGetConnectedAccount, getConnectedAccount as libGetConnectedAccount,
deleteConnectedAccount as libDeleteConnectedAccount, deleteConnectedAccount as libDeleteConnectedAccount,
listAuthConfigs as libListAuthConfigs, listAuthConfigs as libListAuthConfigs,
@ -20,6 +23,7 @@ import {
ZCredentials, ZCredentials,
} from "@/app/lib/composio/composio"; } from "@/app/lib/composio/composio";
import { ComposioConnectedAccount } from "@/app/lib/types/project_types"; import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import { getProjectConfig, projectAuthCheck } from "./project_actions"; import { getProjectConfig, projectAuthCheck } from "./project_actions";
import { projectsCollection } from "../lib/mongodb"; import { projectsCollection } from "../lib/mongodb";
@ -47,6 +51,24 @@ export async function listTools(projectId: string, toolkitSlug: string, cursor:
return await libListTools(toolkitSlug, cursor); return await libListTools(toolkitSlug, cursor);
} }
// New efficient search functions
export async function searchTools(projectId: string, searchQuery: string, cursor: string | null = null, limit?: number): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
await projectAuthCheck(projectId);
return await libSearchTools(searchQuery, cursor, limit);
}
export async function getToolsByIds(projectId: string, toolSlugs: string[], cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
await projectAuthCheck(projectId);
return await libGetToolsByIds(toolSlugs, cursor);
}
export async function getTool(projectId: string, toolSlug: string): Promise<z.infer<typeof ZTool>> {
await projectAuthCheck(projectId);
return await libGetTool(toolSlug);
}
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> { export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
await projectAuthCheck(projectId); await projectAuthCheck(projectId);
@ -215,12 +237,196 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
const key = `composioConnectedAccounts.${toolkitSlug}`; const key = `composioConnectedAccounts.${toolkitSlug}`;
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } }); await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
// Notify other tabs about the tools update (lightweight refresh)
if (typeof window !== 'undefined') {
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
}
return true; return true;
} }
// Note: composio tools are now stored in workflow.tools array with isComposio: true
// This function provides backward compatibility by updating workflow tools
export async function getComposioToolsFromWorkflow(projectId: string): Promise<z.infer<typeof ZTool>[]> {
await projectAuthCheck(projectId);
// Get the project to access draft workflow
const project = await projectsCollection.findOne({ _id: projectId });
if (!project || !project.draftWorkflow) {
return [];
}
// Extract composio tools from workflow and convert back to ZTool format
const composioTools = project.draftWorkflow.tools
.filter(tool => tool.isComposio && tool.composioData)
.map(tool => ({
slug: tool.composioData!.slug,
name: tool.name,
description: tool.description,
no_auth: tool.composioData!.noAuth,
input_parameters: {
type: 'object' as const,
properties: tool.parameters.properties,
required: tool.parameters.required || []
},
toolkit: {
name: tool.composioData!.toolkitName,
slug: tool.composioData!.toolkitSlug,
logo: tool.composioData!.logo,
}
}));
return composioTools;
}
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> { export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
await projectAuthCheck(projectId); await projectAuthCheck(projectId);
// update project with new selected tools // Get the project to access draft workflow
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } }); const project = await projectsCollection.findOne({ _id: projectId });
if (!project || !project.draftWorkflow) {
throw new Error(`Project ${projectId} not found or has no draft workflow`);
}
// Convert Composio tools to workflow tool format
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = tools.map(tool => ({
name: tool.slug,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.input_parameters?.properties || {},
required: tool.input_parameters?.required || []
},
isComposio: true,
composioData: {
slug: tool.slug,
noAuth: tool.no_auth,
toolkitName: tool.toolkit.name,
toolkitSlug: tool.toolkit.slug,
logo: tool.toolkit.logo,
},
}));
// Remove existing composio tools and add new ones
const nonComposioTools = project.draftWorkflow.tools.filter(tool => !tool.isComposio);
const updatedWorkflow = {
...project.draftWorkflow,
tools: [...nonComposioTools, ...composioWorkflowTools],
lastUpdatedAt: new Date().toISOString()
};
// Update the project's draft workflow
const result = await projectsCollection.updateOne(
{ _id: projectId },
{ $set: { draftWorkflow: updatedWorkflow } }
);
if (result.modifiedCount === 0) {
throw new Error(`Failed to update workflow for project ${projectId}`);
}
// Notify other tabs about the tools update (lightweight refresh)
if (typeof window !== 'undefined') {
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
}
}
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
// This function provides backward compatibility by updating workflow mock states
export async function toggleMockToolkitState(projectId: string, toolkitSlug: string, isMocked: boolean, mockInstructions?: string): Promise<void> {
await projectAuthCheck(projectId);
// Get the project to access draft workflow
const project = await projectsCollection.findOne({ _id: projectId });
if (!project || !project.draftWorkflow) {
throw new Error(`Project ${projectId} not found or has no draft workflow`);
}
const now = new Date().toISOString();
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
if (isMocked) {
// Enable mock mode
updatedMockToolkitStates[toolkitSlug] = {
toolkitSlug,
isMocked: true,
mockInstructions: mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.',
autoSubmitMockedResponse: false,
createdAt: now,
lastUpdatedAt: now,
};
} else {
// Disable mock mode - remove the toolkit from the object
delete updatedMockToolkitStates[toolkitSlug];
}
// Update the workflow with new mock states
const updatedWorkflow = {
...project.draftWorkflow,
composioMockToolkitStates: updatedMockToolkitStates,
lastUpdatedAt: now
};
// Update the project's draft workflow
const result = await projectsCollection.updateOne(
{ _id: projectId },
{ $set: { draftWorkflow: updatedWorkflow } }
);
if (result.modifiedCount === 0) {
throw new Error(`Failed to update workflow mock states for project ${projectId}`);
}
// Notify other tabs about the tools update (lightweight refresh)
if (typeof window !== 'undefined') {
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
}
}
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
// This function provides backward compatibility by updating workflow mock states
export async function updateMockToolkitInstructions(projectId: string, toolkitSlug: string, mockInstructions: string): Promise<void> {
await projectAuthCheck(projectId);
// Get the project to access draft workflow
const project = await projectsCollection.findOne({ _id: projectId });
if (!project || !project.draftWorkflow) {
throw new Error(`Project ${projectId} not found or has no draft workflow`);
}
const now = new Date().toISOString();
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
// Update the mock instructions for the specified toolkit
if (updatedMockToolkitStates[toolkitSlug]) {
updatedMockToolkitStates[toolkitSlug] = {
...updatedMockToolkitStates[toolkitSlug],
mockInstructions,
lastUpdatedAt: now
};
// Update the workflow with new mock states
const updatedWorkflow = {
...project.draftWorkflow,
composioMockToolkitStates: updatedMockToolkitStates,
lastUpdatedAt: now
};
// Update the project's draft workflow
const result = await projectsCollection.updateOne(
{ _id: projectId },
{ $set: { draftWorkflow: updatedWorkflow } }
);
if (result.modifiedCount === 0) {
throw new Error(`Failed to update workflow mock instructions for project ${projectId}`);
}
// Notify other tabs about the tools update
if (typeof window !== 'undefined') {
localStorage.setItem(`tools-updated-${projectId}`, Date.now().toString());
}
} else {
throw new Error(`Mock toolkit state for ${toolkitSlug} not found in project ${projectId}`);
}
} }

View file

@ -16,6 +16,13 @@ import { authorizeUserAction } from "./billing_actions";
import { Workflow } from "../lib/types/workflow_types"; import { Workflow } from "../lib/types/workflow_types";
import { WorkflowTool } from "../lib/types/workflow_types"; import { WorkflowTool } from "../lib/types/workflow_types";
import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools"; import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
import {
searchTools as libSearchTools,
getToolsByIds as libGetToolsByIds,
getTool as libGetTool,
ZTool,
ZToolkit
} from "../lib/composio/composio";
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || ''; const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
@ -306,6 +313,113 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
return { id: projectId }; return { id: projectId };
} }
async function detectAndAddComposioTools(projectId: string, workflow: z.infer<typeof Workflow>) {
// Extract tool mentions from agent instructions
const toolMentionPattern = /\[@tool:([^\]]+)\]\(#mention[^\)]*\)/g;
const mentionedToolNames = new Set<string>();
// Scan all agent instructions for tool mentions
for (const agent of workflow.agents || []) {
const instructions = agent.instructions || "";
let match: RegExpExecArray | null;
while ((match = toolMentionPattern.exec(instructions))) {
mentionedToolNames.add(match[1]);
}
}
if (mentionedToolNames.size === 0) {
return; // No tool mentions found
}
console.log(`Found ${mentionedToolNames.size} tool mentions in workflow:`, Array.from(mentionedToolNames));
// Search for these tools in Composio using the new efficient search methods
const foundTools: z.infer<typeof ZTool>[] = [];
try {
// Method 1: Try to get tools directly by their exact slugs/names
const mentionedToolNamesArray = Array.from(mentionedToolNames);
try {
const directToolsResponse = await libGetToolsByIds(mentionedToolNamesArray);
foundTools.push(...directToolsResponse.items);
console.log(`Found ${directToolsResponse.items.length} tools by direct lookup`);
} catch (error) {
console.log('Direct tool lookup failed, trying search approach');
}
// Method 2: For any remaining tools, use search functionality
const foundToolSlugs = new Set(foundTools.map(tool => tool.slug));
const foundToolNames = new Set(foundTools.map(tool => tool.name));
const remainingToolNames = mentionedToolNamesArray.filter(name =>
!foundToolSlugs.has(name) && !foundToolNames.has(name)
);
for (const toolName of remainingToolNames) {
try {
// Search for tools by name/description
const searchResponse = await libSearchTools(toolName, null, 10);
// Find exact matches by name or slug
const exactMatches = searchResponse.items.filter(tool =>
tool.name === toolName ||
tool.slug === toolName ||
tool.name.toLowerCase() === toolName.toLowerCase() ||
tool.slug.toLowerCase() === toolName.toLowerCase()
);
if (exactMatches.length > 0) {
foundTools.push(...exactMatches);
console.log(`Found ${exactMatches.length} tools for search term "${toolName}"`);
} else {
console.log(`No exact matches found for tool "${toolName}"`);
}
} catch (error) {
console.error(`Error searching for tool "${toolName}":`, error);
}
}
} catch (error) {
console.error('Error searching for Composio tools:', error);
return;
}
if (foundTools.length > 0) {
console.log(`Adding ${foundTools.length} Composio tools to workflow`);
// Remove duplicates based on slug
const uniqueTools = foundTools.filter((tool, index, self) =>
index === self.findIndex(t => t.slug === tool.slug)
);
// Convert Composio tools to workflow tool format
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = uniqueTools.map(tool => ({
name: tool.slug,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.input_parameters?.properties || {},
required: tool.input_parameters?.required || []
},
isComposio: true,
composioData: {
slug: tool.slug,
noAuth: tool.no_auth,
toolkitName: tool.toolkit.name,
toolkitSlug: tool.toolkit.slug,
logo: tool.toolkit.logo,
},
}));
// Add these tools to the workflow.tools array
workflow.tools = [...workflow.tools, ...composioWorkflowTools];
console.log(`Added ${composioWorkflowTools.length} Composio tools to workflow`);
} else {
console.log('No matching Composio tools found for the mentioned tool names');
}
}
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> { export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
const user = await authCheck(); const user = await authCheck();
const workflowJson = formData.get('workflowJson') as string; const workflowJson = formData.get('workflowJson') as string;
@ -327,6 +441,15 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
return response; return response;
} }
const projectId = response.id; const projectId = response.id;
// Automatically detect and add Composio tools mentioned in agent instructions
try {
await detectAndAddComposioTools(projectId, workflow);
} catch (error) {
// Log error but don't fail the import if tool detection fails
console.error('Failed to auto-detect Composio tools:', error);
}
return { id: projectId }; return { id: projectId };
} }

View file

@ -17,6 +17,7 @@ import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } f
import { qdrantClient } from '../lib/qdrant'; import { qdrantClient } from '../lib/qdrant';
import { EmbeddingRecord } from "./types/datasource_types"; import { EmbeddingRecord } from "./types/datasource_types";
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types"; import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
import { Project } from "./types/project_types";
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions"; import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
import { PrefixLogger } from "./utils"; import { PrefixLogger } from "./utils";
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types"; import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
@ -280,6 +281,8 @@ async function invokeComposioTool(
name: string, name: string,
composioData: z.infer<typeof WorkflowTool>['composioData'] & {}, composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
input: any, input: any,
workflow: z.infer<typeof Workflow>,
toolDescription?: string,
) { ) {
logger = logger.child(`invokeComposioTool`); logger = logger.child(`invokeComposioTool`);
logger.log(`projectId: ${projectId}`); logger.log(`projectId: ${projectId}`);
@ -288,12 +291,36 @@ async function invokeComposioTool(
const { slug, toolkitSlug, noAuth } = composioData; const { slug, toolkitSlug, noAuth } = composioData;
let connectedAccountId: string | undefined = undefined; // Get project configuration to check for connected accounts (still stored in project)
if (!noAuth) {
const project = await projectsCollection.findOne({ _id: projectId }); const project = await projectsCollection.findOne({ _id: projectId });
if (!project) { if (!project) {
throw new Error(`project ${projectId} not found`); throw new Error(`project ${projectId} not found`);
} }
// Check if toolkit is in mock mode (now from workflow)
const mockState = workflow.composioMockToolkitStates?.[toolkitSlug];
if (mockState?.isMocked) {
logger.log(`toolkit ${toolkitSlug} is in mock mode, using mock response`);
// Use the existing invokeMockTool function to generate a mock response
const mockInstructions = mockState.mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.';
const description = toolDescription || `${name} tool from ${toolkitSlug} toolkit`;
const mockResponse = await invokeMockTool(
logger,
name,
JSON.stringify(input),
description,
mockInstructions
);
logger.log(`mock tool result: ${mockResponse}`);
return mockResponse;
}
// Normal execution path - check for authentication
let connectedAccountId: string | undefined = undefined;
if (!noAuth) {
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id; connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
if (!connectedAccountId) { if (!connectedAccountId) {
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`); throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
@ -452,7 +479,8 @@ function createMcpTool(
function createComposioTool( function createComposioTool(
logger: PrefixLogger, logger: PrefixLogger,
config: z.infer<typeof WorkflowTool>, config: z.infer<typeof WorkflowTool>,
projectId: string projectId: string,
workflow: z.infer<typeof Workflow>
): Tool { ): Tool {
const { name, description, parameters, composioData } = config; const { name, description, parameters, composioData } = config;
@ -472,7 +500,7 @@ function createComposioTool(
}, },
async execute(input: any) { async execute(input: any) {
try { try {
const result = await invokeComposioTool(logger, projectId, name, composioData, input); const result = await invokeComposioTool(logger, projectId, name, composioData, input, workflow, description);
return JSON.stringify({ return JSON.stringify({
result, result,
}); });
@ -813,7 +841,7 @@ function createTools(
tools[toolName] = createMcpTool(logger, config, projectId); tools[toolName] = createMcpTool(logger, config, projectId);
logger.log(`created mcp tool: ${toolName}`); logger.log(`created mcp tool: ${toolName}`);
} else if (config.isComposio) { } else if (config.isComposio) {
tools[toolName] = createComposioTool(logger, config, projectId); tools[toolName] = createComposioTool(logger, config, projectId, workflow);
logger.log(`created composio tool: ${toolName}`); logger.log(`created composio tool: ${toolName}`);
} else if (config.mockTool) { } else if (config.mockTool) {
tools[toolName] = createMockTool(logger, config); tools[toolName] = createMockTool(logger, config);

View file

@ -291,6 +291,39 @@ export async function listTools(toolkitSlug: string, cursor: string | null = nul
return composioApiCall(ZListResponse(ZTool), url.toString()); return composioApiCall(ZListResponse(ZTool), url.toString());
} }
export async function searchTools(searchQuery: string, cursor: string | null = null, limit: number = 50): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const url = new URL(`${BASE_URL}/tools`);
// set params
url.searchParams.set("search", searchQuery);
if (cursor) {
url.searchParams.set("cursor", cursor);
}
url.searchParams.set("limit", limit.toString());
// fetch
return composioApiCall(ZListResponse(ZTool), url.toString());
}
export async function getToolsByIds(toolSlugs: string[], cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const url = new URL(`${BASE_URL}/tools`);
// set params - pass tool slugs as comma-separated string
url.searchParams.set("tool_slugs", toolSlugs.join(","));
if (cursor) {
url.searchParams.set("cursor", cursor);
}
// fetch
return composioApiCall(ZListResponse(ZTool), url.toString());
}
export async function getTool(toolSlug: string): Promise<z.infer<typeof ZTool>> {
const url = new URL(`${BASE_URL}/tools/${toolSlug}`);
return composioApiCall(ZTool, url.toString());
}
export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> { export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
const url = new URL(`${BASE_URL}/auth_configs`); const url = new URL(`${BASE_URL}/auth_configs`);
url.searchParams.set("toolkit_slug", toolkitSlug); url.searchParams.set("toolkit_slug", toolkitSlug);

View file

@ -33,28 +33,8 @@ export async function collectProjectTools(projectId: string): Promise<z.infer<ty
} }
} }
// Add Composio tools // Note: Composio tools are now stored in workflow.tools array with isComposio: true
if (project.composioSelectedTools) { // This function now only collects MCP tools since composio tools are managed in workflow
for (const tool of project.composioSelectedTools) {
tools.push({
name: tool.slug,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.input_parameters?.properties || {},
required: tool.input_parameters?.required || []
},
isComposio: true,
composioData: {
slug: tool.slug,
noAuth: tool.no_auth,
toolkitName: tool.toolkit.name,
toolkitSlug: tool.toolkit.slug,
logo: tool.toolkit.logo,
},
});
}
}
return tools; return tools;
} }

View file

@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { MCPServer } from "./types"; import { MCPServer } from "./types";
import { Workflow, WorkflowTool } from "./workflow_types"; import { Workflow, WorkflowTool } from "./workflow_types";
import { ZTool } from "../composio/composio";
export const ComposioConnectedAccount = z.object({ export const ComposioConnectedAccount = z.object({
id: z.string(), id: z.string(),
@ -30,7 +29,6 @@ export const Project = z.object({
testRunCounter: z.number().default(0), testRunCounter: z.number().default(0),
mcpServers: z.array(MCPServer).optional(), mcpServers: z.array(MCPServer).optional(),
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(), composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
composioSelectedTools: z.array(ZTool).optional(),
}); });
export const ProjectMember = z.object({ export const ProjectMember = z.object({

View file

@ -67,6 +67,14 @@ export const Workflow = z.object({
startAgent: z.string(), startAgent: z.string(),
lastUpdatedAt: z.string().datetime(), lastUpdatedAt: z.string().datetime(),
mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions
composioMockToolkitStates: z.record(z.string(), z.object({
toolkitSlug: z.string(),
isMocked: z.boolean(),
mockInstructions: z.string().optional(),
autoSubmitMockedResponse: z.boolean().default(false),
createdAt: z.string().datetime(),
lastUpdatedAt: z.string().datetime(),
})).optional(),
}); });
export const WorkflowTemplate = Workflow export const WorkflowTemplate = Workflow
.omit({ .omit({

View file

@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search } from 'lucide-react'; import { Info, RefreshCw, Search } from 'lucide-react';
import clsx from 'clsx'; import clsx from 'clsx';
import { listToolkits, listTools, updateComposioSelectedTools } from '@/app/actions/composio_actions'; import { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions';
import { getProjectConfig } from '@/app/actions/project_actions'; import { getProjectConfig } from '@/app/actions/project_actions';
import { z } from 'zod'; import { z } from 'zod';
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio'; import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
@ -30,6 +30,7 @@ export function Composio() {
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null); const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false); const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
const [savingTools, setSavingTools] = useState(false); const [savingTools, setSavingTools] = useState(false);
const [composioSelectedTools, setComposioSelectedTools] = useState<z.infer<typeof ZTool>[]>([]);
const loadProjectConfig = useCallback(async () => { const loadProjectConfig = useCallback(async () => {
try { try {
@ -41,6 +42,15 @@ export function Composio() {
} }
}, [projectId]); }, [projectId]);
const loadComposioSelectedTools = useCallback(async () => {
try {
const tools = await getComposioToolsFromWorkflow(projectId);
setComposioSelectedTools(tools);
} catch (err: any) {
console.error('Error fetching composio selected tools:', err);
}
}, [projectId]);
const loadAllToolkits = useCallback(async () => { const loadAllToolkits = useCallback(async () => {
let cursor: string | null = null; let cursor: string | null = null;
let allToolkits: ToolkitType[] = []; let allToolkits: ToolkitType[] = [];
@ -87,15 +97,16 @@ export function Composio() {
const handleProjectConfigUpdate = useCallback(() => { const handleProjectConfigUpdate = useCallback(() => {
loadProjectConfig(); loadProjectConfig();
}, [loadProjectConfig]); loadComposioSelectedTools();
}, [loadProjectConfig, loadComposioSelectedTools]);
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => { const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
if (!projectId) return; if (!projectId) return;
setSavingTools(true); setSavingTools(true);
try { try {
// Get existing selected tools from project config // Get existing selected tools from workflow
const existingSelectedTools = projectConfig?.composioSelectedTools || []; const existingSelectedTools = composioSelectedTools;
// Create a map of existing tools by slug for easy lookup // Create a map of existing tools by slug for easy lookup
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool])); const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
@ -110,22 +121,22 @@ export function Composio() {
await updateComposioSelectedTools(projectId, mergedSelectedTools); await updateComposioSelectedTools(projectId, mergedSelectedTools);
// Refresh project config to get updated data // Refresh data to get updated tools
await loadProjectConfig(); await loadComposioSelectedTools();
} catch (error) { } catch (error) {
console.error('Error saving tool selection:', error); console.error('Error saving tool selection:', error);
} finally { } finally {
setSavingTools(false); setSavingTools(false);
} }
}, [projectId, projectConfig, loadProjectConfig]); }, [projectId, composioSelectedTools, loadComposioSelectedTools]);
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => { const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
if (!projectId) return; if (!projectId) return;
setSavingTools(true); setSavingTools(true);
try { try {
// Get existing selected tools from project config // Get existing selected tools from workflow
const existingSelectedTools = projectConfig?.composioSelectedTools || []; const existingSelectedTools = composioSelectedTools;
// Filter out all tools from the specified toolkit // Filter out all tools from the specified toolkit
const filteredSelectedTools = existingSelectedTools.filter(tool => const filteredSelectedTools = existingSelectedTools.filter(tool =>
@ -134,18 +145,19 @@ export function Composio() {
await updateComposioSelectedTools(projectId, filteredSelectedTools); await updateComposioSelectedTools(projectId, filteredSelectedTools);
// Refresh project config to get updated data // Refresh data to get updated tools
await loadProjectConfig(); await loadComposioSelectedTools();
} catch (error) { } catch (error) {
console.error('Error removing toolkit tools:', error); console.error('Error removing toolkit tools:', error);
} finally { } finally {
setSavingTools(false); setSavingTools(false);
} }
}, [projectId, projectConfig, loadProjectConfig]); }, [projectId, composioSelectedTools, loadComposioSelectedTools]);
useEffect(() => { useEffect(() => {
loadProjectConfig(); loadProjectConfig();
}, [loadProjectConfig]); loadComposioSelectedTools();
}, [loadProjectConfig, loadComposioSelectedTools]);
useEffect(() => { useEffect(() => {
loadAllToolkits(); loadAllToolkits();

View file

@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import { PictureImg } from '@/components/ui/picture-img'; import { PictureImg } from '@/components/ui/picture-img';
import { Button, Checkbox } from '@heroui/react'; import { Button, Checkbox } from '@heroui/react';
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react'; import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions'; import { listTools, deleteConnectedAccount, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions';
import { z } from 'zod'; import { z } from 'zod';
import { ZTool, ZListResponse } from '@/app/lib/composio/composio'; import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
import { SlidePanel } from '@/components/ui/slide-panel'; import { SlidePanel } from '@/components/ui/slide-panel';
@ -57,6 +57,7 @@ export function ComposioToolsPanel({
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [showAuthModal, setShowAuthModal] = useState(false); const [showAuthModal, setShowAuthModal] = useState(false);
const [isProcessingAuth, setIsProcessingAuth] = useState(false); const [isProcessingAuth, setIsProcessingAuth] = useState(false);
const [composioSelectedTools, setComposioSelectedTools] = useState<ToolType[]>([]);
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => { const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
try { try {
@ -80,6 +81,15 @@ export function ComposioToolsPanel({
} }
}, [projectId]); }, [projectId]);
const loadComposioSelectedTools = useCallback(async () => {
try {
const tools = await getComposioToolsFromWorkflow(projectId);
setComposioSelectedTools(tools);
} catch (err: any) {
console.error('Error fetching composio selected tools:', err);
}
}, [projectId]);
const handleNextPage = useCallback(async () => { const handleNextPage = useCallback(async () => {
if (!nextCursor || !toolkit) return; if (!nextCursor || !toolkit) return;
@ -164,14 +174,21 @@ export function ComposioToolsPanel({
} }
}, [onClose, hasChanges]); }, [onClose, hasChanges]);
// Initialize selected tools from project config when opening the panel // Initialize selected tools from workflow when opening the panel
useEffect(() => { useEffect(() => {
if (toolkit && isOpen && projectConfig?.composioSelectedTools) { if (toolkit && isOpen) {
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug)); loadComposioSelectedTools();
}
}, [toolkit, isOpen, loadComposioSelectedTools]);
// Set selected tools when composioSelectedTools is loaded
useEffect(() => {
if (toolkit && composioSelectedTools.length > 0) {
const toolSlugs = new Set(composioSelectedTools.map(tool => tool.slug));
setSelectedTools(toolSlugs); setSelectedTools(toolSlugs);
setHasChanges(false); setHasChanges(false);
} }
}, [toolkit, isOpen, projectConfig]); }, [toolkit, composioSelectedTools]);
useEffect(() => { useEffect(() => {
if (toolkit && isOpen) { if (toolkit && isOpen) {
@ -210,44 +227,46 @@ export function ComposioToolsPanel({
<div className={`mb-6 p-4 rounded-lg border-2 ${ <div className={`mb-6 p-4 rounded-lg border-2 ${
isToolkitConnected isToolkitConnected
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800' ? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800' : 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
}`}> }`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${ <div className={`w-3 h-3 rounded-full ${
isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500' isToolkitConnected ? 'bg-emerald-500' : 'bg-blue-500'
}`}></div> }`}></div>
<div> <div>
<h3 className={`font-semibold text-sm ${ <h3 className={`font-semibold text-sm ${
isToolkitConnected isToolkitConnected
? 'text-emerald-800 dark:text-emerald-200' ? 'text-emerald-800 dark:text-emerald-200'
: 'text-orange-800 dark:text-orange-200' : 'text-blue-800 dark:text-blue-200'
}`}> }`}>
{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'} {isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
</h3> </h3>
<p className={`text-xs mt-0.5 ${ <p className={`text-xs mt-0.5 ${
isToolkitConnected isToolkitConnected
? 'text-emerald-700 dark:text-emerald-300' ? 'text-emerald-700 dark:text-emerald-300'
: 'text-orange-700 dark:text-orange-300' : 'text-blue-700 dark:text-blue-300'
}`}> }`}>
{isToolkitConnected {isToolkitConnected
? 'You can select and use tools from this toolkit' ? 'You can select and use tools from this toolkit'
: 'Connect your account to access and use tools' : 'You can select tools now. Authentication will be required in the build view to use them.'
} }
</p> </p>
</div> </div>
</div> </div>
{isToolkitConnected && (
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
onPress={isToolkitConnected ? handleDisconnect : handleConnect} onPress={handleDisconnect}
disabled={isProcessingAuth} disabled={isProcessingAuth}
color={isToolkitConnected ? "danger" : "primary"} color="danger"
isLoading={isProcessingAuth} isLoading={isProcessingAuth}
startContent={isToolkitConnected ? <UnlinkIcon className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />} startContent={<UnlinkIcon className="h-4 w-4" />}
> >
{isToolkitConnected ? 'Disconnect' : 'Connect Now'} Disconnect
</Button> </Button>
)}
</div> </div>
</div> </div>
)} )}
@ -263,7 +282,7 @@ export function ComposioToolsPanel({
size="sm" size="sm"
color="primary" color="primary"
onPress={handleSaveTools} onPress={handleSaveTools}
disabled={isSaving || !isToolkitConnected} disabled={isSaving}
isLoading={isSaving} isLoading={isSaving}
> >
Save Changes Save Changes
@ -283,17 +302,12 @@ export function ComposioToolsPanel({
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{tools.map((tool) => ( {tools.map((tool) => (
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${ <div key={tool.slug} className="group p-4 rounded-lg transition-all duration-200 border border-transparent bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600">
isToolkitConnected
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Checkbox <Checkbox
isSelected={selectedTools.has(tool.slug)} isSelected={selectedTools.has(tool.slug)}
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)} onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
size="sm" size="sm"
isDisabled={!isToolkitConnected}
/> />
<div className="flex-1"> <div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"> <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">

View file

@ -24,7 +24,7 @@ import { Modal } from '@/components/ui/modal';
type McpServerType = z.infer<typeof MCPServer>; type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof MCPServer>['tools'][number]; type McpToolType = z.infer<typeof MCPServer>['tools'][number];
export function CustomServers() { export function CustomServers({ onToolsUpdated }: { onToolsUpdated?: () => void }) {
const params = useParams(); const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required'); if (!projectId) throw new Error('Project ID is required');
@ -92,6 +92,9 @@ export function CustomServers() {
return s; return s;
}); });
}); });
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) { } catch (err) {
console.error('Toggle failed:', { server: server.name, error: err }); console.error('Toggle failed:', { server: server.name, error: err });
} finally { } finally {
@ -161,6 +164,9 @@ export function CustomServers() {
// Update selectedTools to include all tools for the custom server // Update selectedTools to include all tools for the custom server
setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id))); setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id)));
} }
// Notify parent component about tool updates
onToolsUpdated?.();
} finally { } finally {
setSyncingServers(prev => { setSyncingServers(prev => {
const next = new Set(prev); const next = new Set(prev);
@ -207,6 +213,9 @@ export function CustomServers() {
// Fetch tools for the new server using the formatted URL // Fetch tools for the new server using the formatted URL
await handleSyncServer(formattedServer); await handleSyncServer(formattedServer);
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) { } catch (err) {
console.error('Error adding server:', err); console.error('Error adding server:', err);
setError('Failed to add server. Please try again.'); setError('Failed to add server. Please try again.');
@ -229,6 +238,9 @@ export function CustomServers() {
if (selectedServer?.name === server.name) { if (selectedServer?.name === server.name) {
setSelectedServer(null); setSelectedServer(null);
} }
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) { } catch (err) {
console.error('Error removing server:', err); console.error('Error removing server:', err);
setError('Failed to remove server. Please try again.'); setError('Failed to remove server. Please try again.');
@ -271,6 +283,9 @@ export function CustomServers() {
}); });
setHasToolChanges(false); setHasToolChanges(false);
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (error) { } catch (error) {
console.error('Error saving tool selection:', error); console.error('Error saving tool selection:', error);
} finally { } finally {

View file

@ -52,9 +52,8 @@ export function ToolkitCard({
}, [onManageTools]); }, [onManageTools]);
// Calculate selected tools count for this toolkit // Calculate selected tools count for this toolkit
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool => // TODO: Update to use workflow-based tools count
tool.toolkit.slug === toolkit.slug const selectedToolsCount = 0;
).length || 0;
return ( return (
<div className={toolkitCardStyles.base} onClick={handleCardClick}> <div className={toolkitCardStyles.base} onClick={handleCardClick}>

View file

@ -16,7 +16,7 @@ export default async function ToolsPage() {
<div className="flex-1 p-6"> <div className="flex-1 p-6">
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<ToolsConfig <ToolsConfig
useComposioTools={USE_COMPOSIO_TOOLS} useComposioTools={false}
useKlavisTools={USE_KLAVIS_TOOLS} useKlavisTools={USE_KLAVIS_TOOLS}
/> />
</Suspense> </Suspense>

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { MCPServer, WithStringId } from "../../../lib/types/types"; import { MCPServer, WithStringId } from "../../../lib/types/types";
import { DataSource } from "../../../lib/types/datasource_types"; import { DataSource } from "../../../lib/types/datasource_types";
import { Project } from "../../../lib/types/project_types";
import { z } from "zod"; import { z } from "zod";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { WorkflowEditor } from "./workflow_editor"; import { WorkflowEditor } from "./workflow_editor";
@ -11,7 +12,6 @@ import { getProjectConfig } from "@/app/actions/project_actions";
import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types"; import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types";
import { getEligibleModels } from "@/app/actions/billing_actions"; import { getEligibleModels } from "@/app/actions/billing_actions";
import { ModelsResponse } from "@/app/lib/types/billing_types"; import { ModelsResponse } from "@/app/lib/types/billing_types";
import { Project } from "@/app/lib/types/project_types";
export function App({ export function App({
projectId, projectId,
@ -26,6 +26,7 @@ export function App({
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null); const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null); const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null);
const [projectTools, setProjectTools] = useState<z.infer<typeof WorkflowTool>[] | null>(null); const [projectTools, setProjectTools] = useState<z.infer<typeof WorkflowTool>[] | null>(null);
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*"); const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*");
const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]); const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]);
@ -66,11 +67,70 @@ export function App({
setLoading(false); setLoading(false);
}, [projectId]); }, [projectId]);
const handleProjectToolsUpdate = useCallback(async () => {
// Lightweight refresh for tool-only updates
const [projectConfig, projectTools] = await Promise.all([
getProjectConfig(projectId),
collectProjectTools(projectId),
]);
setProject(projectConfig);
setProjectConfig(projectConfig);
setProjectTools(projectTools);
// Update MCP servers if they changed
if (projectConfig.mcpServers) {
setProjectMcpServers(projectConfig.mcpServers);
}
// Update webhook URL if it changed
if (projectConfig.webhookUrl) {
setWebhookUrl(projectConfig.webhookUrl);
}
}, [projectId]);
// Add this useEffect for initial load // Add this useEffect for initial load
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [mode, loadData, projectId]); }, [mode, loadData, projectId]);
// Add focus-based refresh to handle cross-page updates
useEffect(() => {
const handleFocus = () => {
// Refresh data when user returns to this page/tab
loadData();
};
const handleVisibilityChange = () => {
if (!document.hidden) {
loadData();
}
};
const handleStorageChange = (e: StorageEvent) => {
// Listen for tool updates from other tabs
if (e.key === `tools-updated-${projectId}` && e.newValue) {
loadData();
// Clear the flag
localStorage.removeItem(`tools-updated-${projectId}`);
} else if (e.key === `tools-light-refresh-${projectId}` && e.newValue) {
// Lightweight refresh for tool-only updates
handleProjectToolsUpdate();
// Clear the flag
localStorage.removeItem(`tools-light-refresh-${projectId}`);
}
};
window.addEventListener('focus', handleFocus);
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('focus', handleFocus);
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('storage', handleStorageChange);
};
}, [loadData, handleProjectToolsUpdate, projectId]);
function handleSetMode(mode: 'draft' | 'live') { function handleSetMode(mode: 'draft' | 'live') {
setMode(mode); setMode(mode);
} }
@ -95,6 +155,7 @@ export function App({
workflow={workflow} workflow={workflow}
dataSources={dataSources} dataSources={dataSources}
projectTools={projectTools} projectTools={projectTools}
projectConfig={projectConfig || project}
useRag={useRag} useRag={useRag}
mcpServerUrls={projectMcpServers} mcpServerUrls={projectMcpServers}
toolWebhookUrl={webhookUrl} toolWebhookUrl={webhookUrl}
@ -102,6 +163,7 @@ export function App({
eligibleModels={eligibleModels} eligibleModels={eligibleModels}
onChangeMode={handleSetMode} onChangeMode={handleSetMode}
onRevertToLive={handleRevertToLive} onRevertToLive={handleRevertToLive}
onProjectToolsUpdated={handleProjectToolsUpdate}
/>} />}
</> </>
} }

View file

@ -0,0 +1,74 @@
'use client';
import React, { useState } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, Tabs, Tab } from '@heroui/react';
import { Composio } from '../../tools/components/Composio';
import { ComposioWithCallback } from './ComposioWithCallback';
import { CustomServers } from '../../tools/components/CustomServers';
import { WebhookConfig } from '../../tools/components/WebhookConfig';
import type { Key } from 'react';
interface ComposioToolsModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onToolsUpdated?: () => void;
}
export function ComposioToolsModal({ isOpen, onClose, projectId, onToolsUpdated }: ComposioToolsModalProps) {
const [activeTab, setActiveTab] = useState('composio');
const handleTabChange = (key: Key) => {
setActiveTab(key.toString());
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="5xl"
scrollBehavior="inside"
classNames={{
base: "max-h-[90vh]",
body: "p-0",
header: "pb-3"
}}
>
<ModalContent>
<ModalHeader>
<h3 className="text-lg font-semibold">
Tools
</h3>
</ModalHeader>
<ModalBody>
<Tabs
selectedKey={activeTab}
onSelectionChange={handleTabChange}
aria-label="Tool configuration options"
className="w-full h-full"
fullWidth
classNames={{
panel: "h-full min-h-[60vh]"
}}
>
<Tab key="composio" title="Composio">
<div className="p-6 h-full">
<ComposioWithCallback projectId={projectId} onToolsUpdated={onToolsUpdated} />
</div>
</Tab>
<Tab key="custom" title="Custom MCP Servers">
<div className="p-6 h-full">
<CustomServers onToolsUpdated={onToolsUpdated} />
</div>
</Tab>
<Tab key="webhook" title="Webhook">
<div className="p-6 h-full">
<WebhookConfig />
</div>
</Tab>
</Tabs>
</ModalBody>
</ModalContent>
</Modal>
);
}

View file

@ -0,0 +1,279 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search } from 'lucide-react';
import clsx from 'clsx';
import { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions';
import { getProjectConfig } from '@/app/actions/project_actions';
import { z } from 'zod';
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
import { Project } from '@/app/lib/types/project_types';
import { ComposioToolsPanel } from '../../tools/components/ComposioToolsPanel';
import { ToolkitCard } from '../../tools/components/ToolkitCard';
type ToolkitType = z.infer<typeof ZToolkit>;
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
type ProjectType = z.infer<typeof Project>;
interface ComposioWithCallbackProps {
projectId: string;
onToolsUpdated?: () => void;
}
export function ComposioWithCallback({ projectId, onToolsUpdated }: ComposioWithCallbackProps) {
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
const [savingTools, setSavingTools] = useState(false);
const [composioSelectedTools, setComposioSelectedTools] = useState<z.infer<typeof ZTool>[]>([]);
const loadProjectConfig = useCallback(async () => {
try {
const config = await getProjectConfig(projectId);
setProjectConfig(config);
} catch (err: any) {
console.error('Error fetching project config:', err);
setError('Unable to load project configuration.');
}
}, [projectId]);
const loadComposioSelectedTools = useCallback(async () => {
try {
const tools = await getComposioToolsFromWorkflow(projectId);
setComposioSelectedTools(tools);
} catch (err: any) {
console.error('Error fetching composio selected tools:', err);
}
}, [projectId]);
const loadAllToolkits = useCallback(async () => {
let cursor: string | null = null;
let allToolkits: ToolkitType[] = [];
try {
setLoading(true);
do {
const response: ToolkitListResponse = await listToolkits(projectId, cursor);
allToolkits = [...allToolkits, ...response.items];
cursor = response.next_cursor;
} while (cursor !== null);
setToolkits(allToolkits);
setError(null);
} catch (err: any) {
setError('Unable to load all Composio toolkits. Please check your connection and try again.');
console.error('Error fetching all toolkits:', err);
setToolkits([]);
} finally {
setLoading(false);
}
}, [projectId]);
const handleManageTools = useCallback((toolkit: ToolkitType) => {
setSelectedToolkit(toolkit);
setIsToolsPanelOpen(true);
}, []);
const handleCloseToolsPanel = useCallback(() => {
setSelectedToolkit(null);
setIsToolsPanelOpen(false);
}, []);
const handleProjectConfigUpdate = useCallback(() => {
loadProjectConfig();
loadComposioSelectedTools();
}, [loadProjectConfig, loadComposioSelectedTools]);
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from workflow
const existingSelectedTools = composioSelectedTools;
// Create a map of existing tools by slug for easy lookup
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
// Add or update the new selections
for (const tool of selectedToolObjects) {
existingToolsMap.set(tool.slug, tool);
}
// Convert back to array
const mergedSelectedTools = Array.from(existingToolsMap.values());
await updateComposioSelectedTools(projectId, mergedSelectedTools);
// Refresh data to get updated tools
await loadComposioSelectedTools();
// Notify parent component that tools were updated
if (onToolsUpdated) {
onToolsUpdated();
}
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {
setSavingTools(false);
}
}, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]);
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from workflow
const existingSelectedTools = composioSelectedTools;
// Filter out all tools from the specified toolkit
const filteredSelectedTools = existingSelectedTools.filter(tool =>
tool.toolkit.slug !== toolkitSlug
);
await updateComposioSelectedTools(projectId, filteredSelectedTools);
// Refresh data to get updated tools
await loadComposioSelectedTools();
// Notify parent component that tools were updated
if (onToolsUpdated) {
onToolsUpdated();
}
} catch (error) {
console.error('Error removing toolkit tools:', error);
} finally {
setSavingTools(false);
}
}, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]);
useEffect(() => {
loadProjectConfig();
}, [loadProjectConfig]);
useEffect(() => {
loadAllToolkits();
loadComposioSelectedTools();
}, [loadAllToolkits, loadComposioSelectedTools]);
const filteredToolkits = toolkits.filter(toolkit => {
const searchLower = searchQuery.toLowerCase();
return (
toolkit.name.toLowerCase().includes(searchLower) ||
toolkit.meta.description.toLowerCase().includes(searchLower) ||
toolkit.slug.toLowerCase().includes(searchLower)
);
}).sort((a, b) => {
// Sort by actual connection status first (only connected tools, not no-auth)
const aConnected = !a.no_auth && projectConfig?.composioConnectedAccounts?.[a.slug]?.status === 'ACTIVE';
const bConnected = !b.no_auth && projectConfig?.composioConnectedAccounts?.[b.slug]?.status === 'ACTIVE';
if (aConnected && !bConnected) return -1;
if (!aConnected && bConnected) return 1;
// If both have same connection status, maintain original order (don't sort alphabetically)
return 0;
});
if (loading) {
return (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading Composio toolkits...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
<p className="text-center text-red-500 dark:text-red-400 max-w-[600px]">
{error}
</p>
<Button
variant="secondary"
onClick={() => {
loadProjectConfig();
loadAllToolkits();
}}
>
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
</Button>
</div>
);
}
return (
<div className="h-full flex flex-col">
{/* Search Bar */}
<div className="mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search toolkits..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
placeholder-gray-500 dark:placeholder-gray-400
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
{/* Toolkits Grid */}
<div className="flex-1 overflow-y-auto">
{filteredToolkits.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400">
{searchQuery ? 'No toolkits found matching your search.' : 'No toolkits available.'}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredToolkits.map((toolkit) => {
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
return (
<ToolkitCard
key={toolkit.slug}
toolkit={toolkit}
projectId={projectId}
isConnected={isConnected}
connectedAccountId={connectedAccountId}
projectConfig={projectConfig}
onManageTools={() => handleManageTools(toolkit)}
onProjectConfigUpdate={handleProjectConfigUpdate}
onRemoveToolkitTools={handleRemoveToolkitTools}
/>
);
})}
</div>
)}
</div>
{/* Tools Panel */}
<ComposioToolsPanel
toolkit={selectedToolkit}
isOpen={isToolsPanelOpen}
onClose={handleCloseToolsPanel}
projectConfig={projectConfig}
onUpdateToolsSelection={handleUpdateToolsSelection}
onProjectConfigUpdate={handleProjectConfigUpdate}
onRemoveToolkitTools={handleRemoveToolkitTools}
isSaving={savingTools}
/>
</div>
);
}

View file

@ -1,8 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { WorkflowPrompt, WorkflowAgent, WorkflowTool } from "../../../lib/types/workflow_types"; import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types";
import { Project } from "../../../lib/types/project_types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react";
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, Eye } from "lucide-react"; import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, TestTube, Play, MoreVertical, Eye } from "lucide-react";
import { Tooltip } from "@heroui/react";
import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
@ -13,6 +15,9 @@ import { clsx } from "clsx";
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
import { ServerLogo } from '../tools/components/MCPServersCommon'; import { ServerLogo } from '../tools/components/MCPServersCommon';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react";
import { ComposioToolsModal } from './components/ComposioToolsModal';
import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal';
import { deleteConnectedAccount, toggleMockToolkitState } from '@/app/actions/composio_actions';
// Reduced gap size to match Cursor's UI // Reduced gap size to match Cursor's UI
const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit) const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit)
@ -35,6 +40,7 @@ interface EntityListProps {
tools: z.infer<typeof WorkflowTool>[]; tools: z.infer<typeof WorkflowTool>[];
projectTools: z.infer<typeof WorkflowTool>[]; projectTools: z.infer<typeof WorkflowTool>[];
prompts: z.infer<typeof WorkflowPrompt>[]; prompts: z.infer<typeof WorkflowPrompt>[];
workflow: z.infer<typeof Workflow>;
selectedEntity: { selectedEntity: {
type: "agent" | "tool" | "prompt" | "visualise"; type: "agent" | "tool" | "prompt" | "visualise";
name: string; name: string;
@ -52,6 +58,8 @@ interface EntityListProps {
onDeleteTool: (name: string) => void; onDeleteTool: (name: string) => void;
onDeletePrompt: (name: string) => void; onDeletePrompt: (name: string) => void;
onShowVisualise: (name: string) => void; onShowVisualise: (name: string) => void;
onProjectToolsUpdated?: () => void;
projectConfig?: z.infer<typeof Project>;
} }
interface EmptyStateProps { interface EmptyStateProps {
@ -95,7 +103,7 @@ const ListItemWithMenu = ({
}) => { }) => {
return ( return (
<div className={clsx( <div className={clsx(
"group flex items-center gap-2 px-2 py-1.5 rounded-md", "group flex items-center gap-2 px-2 py-0.5 rounded-md min-h-[24px]",
{ {
"bg-indigo-50 dark:bg-indigo-950/30": isSelected, "bg-indigo-50 dark:bg-indigo-950/30": isSelected,
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected "hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
@ -116,24 +124,22 @@ const ListItemWithMenu = ({
}} }}
disabled={disabled} disabled={disabled}
> >
<div className={clsx("shrink-0 flex items-center justify-center w-4 h-4", iconClassName)}> <div className={clsx("shrink-0 flex items-center justify-center w-3 h-3", iconClassName)}>
{mcpServerName ? ( {mcpServerName ? (
<ServerLogo <ServerLogo
serverName={mcpServerName} serverName={mcpServerName}
className="h-4 w-4" className="h-3 w-3"
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />} fallback={<ImportIcon className="w-3 h-3 text-blue-600 dark:text-blue-500" />}
/> />
) : icon} ) : icon}
</div> </div>
{name} <span className="text-xs">{name}</span>
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
{statusLabel} {statusLabel}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
{menuContent} {menuContent}
</div> </div>
</div> </div>
</div>
); );
}; };
@ -166,43 +172,56 @@ const ServerCard = ({
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
return ( return (
<div className="mb-2"> <div className="mb-1 group">
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
<button <button
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left" className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
> >
{isExpanded ? ( {/* Chevron - only show when has tools and on hover */}
<ChevronDown className="w-4 h-4 text-gray-500" /> <div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
tools.length > 0 ? 'group-hover:opacity-100 opacity-60' : 'opacity-0'
}`}>
{tools.length > 0 && (isExpanded ? (
<ChevronDown className="w-3 h-3 text-gray-500" />
) : ( ) : (
<ChevronRight className="w-4 h-4 text-gray-500" /> <ChevronRight className="w-3 h-3 text-gray-500" />
)} ))}
<div className="flex items-center gap-1"> </div>
<div className="flex items-center gap-2">
<ServerLogo <ServerLogo
serverName={serverName} serverName={serverName}
className="h-4 w-4" className="h-4 w-4"
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />} fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
/> />
<span>{serverName}</span> <span className="text-sm">{serverName}</span>
</div> </div>
</button> </button>
</div>
{isExpanded && ( {isExpanded && (
<div className="ml-6 mt-1 space-y-1"> <div className="ml-6 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
{tools.map((tool, index) => ( {tools.map((tool, index) => (
<div key={`tool-${index}`} className="group/tool">
<ListItemWithMenu <ListItemWithMenu
key={`tool-${index}`}
name={tool.name} name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name} isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
onClick={() => onSelectTool(tool.name)} onClick={() => onSelectTool(tool.name)}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
mcpServerName={serverName} mcpServerName={serverName}
menuContent={ menuContent={
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
<EntityDropdown <EntityDropdown
name={tool.name} name={tool.name}
onDelete={onDeleteTool} onDelete={onDeleteTool}
isLocked={tool.isMcp || tool.isLibrary} isLocked={tool.isMcp || tool.isLibrary}
/> />
</div>
} }
/> />
</div>
))} ))}
</div> </div>
)} )}
@ -222,6 +241,7 @@ export function EntityList({
tools, tools,
projectTools, projectTools,
prompts, prompts,
workflow,
selectedEntity, selectedEntity,
startAgentName, startAgentName,
onSelectAgent, onSelectAgent,
@ -235,7 +255,9 @@ export function EntityList({
onDeleteAgent, onDeleteAgent,
onDeleteTool, onDeleteTool,
onDeletePrompt, onDeletePrompt,
onProjectToolsUpdated,
projectId, projectId,
projectConfig,
onReorderAgents, onReorderAgents,
onShowVisualise, onShowVisualise,
}: EntityListProps & { }: EntityListProps & {
@ -243,6 +265,7 @@ export function EntityList({
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
}) { }) {
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false); const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
const [showComposioToolsModal, setShowComposioToolsModal] = useState(false);
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => { const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
onAddAgent({ onAddAgent({
@ -496,6 +519,21 @@ export function EntityList({
<Wrench className="w-4 h-4" /> <Wrench className="w-4 h-4" />
<span>Tools</span> <span>Tools</span>
</div> </div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setExpandedPanels(prev => ({ ...prev, tools: true }));
setShowComposioToolsModal(true);
}}
className={`group ${buttonClasses}`}
showHoverContent={true}
hoverContent="Composio Tools"
>
<Component className="w-4 h-4" />
</Button>
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
@ -511,6 +549,7 @@ export function EntityList({
<PlusIcon className="w-4 h-4" /> <PlusIcon className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div>
} }
> >
{expandedPanels.tools && ( {expandedPanels.tools && (
@ -536,8 +575,24 @@ export function EntityList({
return ( return (
<> <>
{/* Show composio cards */} {/* Show composio cards - ordered by status */}
{Object.values(composioTools).map((card) => ( {Object.values(composioTools)
.sort((a, b) => {
// Helper function to get toolkit status priority
const getStatusPriority = (toolkit: ComposioToolkit) => {
const hasAuth = toolkit.tools.some(tool => tool.composioData && !tool.composioData.noAuth);
const isConnected = !hasAuth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
const isMocked = workflow?.composioMockToolkitStates?.[toolkit.slug]?.isMocked || false;
// Priority: Connected (1) > Mock (2) > Disconnected (3)
if (isMocked) return 2;
if (isConnected) return 1;
return 3;
};
return getStatusPriority(a) - getStatusPriority(b);
})
.map((card) => (
<ComposioCard <ComposioCard
key={card.slug} key={card.slug}
card={card} card={card}
@ -545,6 +600,10 @@ export function EntityList({
onSelectTool={handleToolSelection} onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool} onDeleteTool={onDeleteTool}
selectedRef={selectedRef} selectedRef={selectedRef}
projectConfig={projectConfig}
projectId={projectId}
workflow={workflow}
onProjectToolsUpdated={onProjectToolsUpdated}
/> />
))} ))}
@ -682,6 +741,12 @@ export function EntityList({
onClose={() => setShowAgentTypeModal(false)} onClose={() => setShowAgentTypeModal(false)}
onConfirm={handleAddAgentWithType} onConfirm={handleAddAgentWithType}
/> />
<ComposioToolsModal
isOpen={showComposioToolsModal}
onClose={() => setShowComposioToolsModal(false)}
projectId={projectId}
onToolsUpdated={onProjectToolsUpdated}
/>
</div> </div>
); );
} }
@ -769,6 +834,10 @@ interface ComposioCardProps {
onSelectTool: (name: string) => void; onSelectTool: (name: string) => void;
onDeleteTool: (name: string) => void; onDeleteTool: (name: string) => void;
selectedRef: React.RefObject<HTMLButtonElement | null>; selectedRef: React.RefObject<HTMLButtonElement | null>;
projectConfig?: z.infer<typeof Project>;
projectId: string;
workflow: z.infer<typeof Workflow>;
onProjectToolsUpdated?: () => void;
} }
const ComposioCard = ({ const ComposioCard = ({
@ -777,21 +846,82 @@ const ComposioCard = ({
onSelectTool, onSelectTool,
onDeleteTool, onDeleteTool,
selectedRef, selectedRef,
projectConfig,
projectId,
workflow,
onProjectToolsUpdated,
}: ComposioCardProps) => { }: ComposioCardProps) => {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [showAuthModal, setShowAuthModal] = useState(false);
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
const [isProcessingMock, setIsProcessingMock] = useState(false);
// Check if the toolkit requires authentication
const hasToolkitWithAuth = card.tools.some(tool => tool.composioData && !tool.composioData.noAuth);
// Check if toolkit is connected
const isToolkitConnected = !hasToolkitWithAuth || projectConfig?.composioConnectedAccounts?.[card.slug]?.status === 'ACTIVE';
// Check if toolkit is mocked
const isToolkitMocked = workflow?.composioMockToolkitStates?.[card.slug]?.isMocked || false;
const handleConnect = () => {
setShowAuthModal(true);
};
const handleDisconnect = async () => {
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id;
setIsProcessingAuth(true);
try {
if (connectedAccountId) {
await deleteConnectedAccount(projectId, card.slug, connectedAccountId);
onProjectToolsUpdated?.();
}
} catch (err: any) {
console.error('Disconnect failed:', err);
} finally {
setIsProcessingAuth(false);
}
};
const handleAuthComplete = () => {
setShowAuthModal(false);
onProjectToolsUpdated?.();
};
const handleToggleMock = async () => {
setIsProcessingMock(true);
try {
await toggleMockToolkitState(projectId, card.slug, !isToolkitMocked);
onProjectToolsUpdated?.();
} catch (err: any) {
console.error('Mock toggle failed:', err);
} finally {
setIsProcessingMock(false);
}
};
return ( return (
<div className="mb-2"> <>
<div className="mb-1 group">
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
<button <button
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left" className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
> >
{isExpanded ? ( {/* Chevron - only show on hover or when has tools */}
<ChevronDown className="w-4 h-4 text-gray-500" /> <div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
card.tools.length > 0 ? 'group-hover:opacity-100 opacity-60' : 'opacity-0'
}`}>
{card.tools.length > 0 && (isExpanded ? (
<ChevronDown className="w-3 h-3 text-gray-500" />
) : ( ) : (
<ChevronRight className="w-4 h-4 text-gray-500" /> <ChevronRight className="w-3 h-3 text-gray-500" />
)} ))}
<div className="flex items-center gap-1"> </div>
<div className="flex items-center gap-2">
{card.logo ? ( {card.logo ? (
<div className="relative w-4 h-4"> <div className="relative w-4 h-4">
<PictureImg <PictureImg
@ -803,44 +933,144 @@ const ComposioCard = ({
) : ( ) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" /> <ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)} )}
<span>{card.name}</span> <span className="text-sm">{card.name}</span>
</div> </div>
</button> </button>
{/* Compact Status Badge */}
<Tooltip
content={isToolkitMocked ? 'Mocked' : isToolkitConnected ? 'Connected' : 'Disconnected'}
size="sm"
delay={500}
>
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium text-white ${
isToolkitMocked
? 'bg-purple-500'
: isToolkitConnected
? 'bg-emerald-500'
: 'bg-orange-500'
}`}>
{isToolkitMocked ? 'M' : isToolkitConnected ? '●' : '○'}
</div>
</Tooltip>
{/* Actions Dropdown - only show on hover */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<Dropdown>
<DropdownTrigger>
<button className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors">
<MoreVertical className="h-3 w-3 text-gray-500" />
</button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
switch (key) {
case 'mock':
handleToggleMock();
break;
case 'connect':
handleConnect();
break;
case 'disconnect':
handleDisconnect();
break;
}
}}
disabledKeys={[
...(isProcessingMock ? ['mock'] : []),
...(isProcessingAuth ? ['connect', 'disconnect'] : []),
...(hasToolkitWithAuth && !isToolkitMocked && isToolkitConnected ? [] : ['disconnect']),
...(hasToolkitWithAuth && !isToolkitConnected ? [] : ['connect'])
]}
>
<DropdownItem
key="mock"
startContent={
isProcessingMock ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : isToolkitMocked ? (
<Play className="h-3 w-3" />
) : (
<TestTube className="h-3 w-3" />
)
}
>
{isProcessingMock
? (isToolkitMocked ? 'Disabling Mock...' : 'Enabling Mock...')
: (isToolkitMocked ? 'Switch to Real Mode' : 'Switch to Mock Mode')
}
</DropdownItem>
<DropdownItem
key="disconnect"
startContent={
isProcessingAuth ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : (
<UnlinkIcon className="h-3 w-3" />
)
}
>
{isProcessingAuth ? 'Disconnecting...' : 'Disconnect'}
</DropdownItem>
<DropdownItem
key="connect"
startContent={
isProcessingAuth ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : (
<LinkIcon className="h-3 w-3" />
)
}
>
{isProcessingAuth ? 'Connecting...' : 'Connect'}
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
{isExpanded && ( {isExpanded && (
<div className="ml-6 mt-1 space-y-1"> <div className="ml-6 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
{card.tools.map((tool, index) => ( {card.tools.map((tool, index) => (
<div key={`composio-tool-${index}`} className="group/tool">
<ListItemWithMenu <ListItemWithMenu
key={`composio-tool-${index}`}
name={tool.name} name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name} isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
onClick={() => onSelectTool(tool.name)} onClick={() => onSelectTool(tool.name)}
disabled={tool.isLibrary} disabled={tool.isLibrary}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
icon={ icon={
card.logo ? ( <div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600"></div>
<div className="relative w-4 h-4">
<PictureImg
src={card.logo}
alt={`${card.name} logo`}
className="w-full h-full object-contain rounded"
/>
</div>
) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)
} }
menuContent={ menuContent={
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
<EntityDropdown <EntityDropdown
name={tool.name} name={tool.name}
onDelete={onDeleteTool} onDelete={onDeleteTool}
isLocked={tool.isComposio} isLocked={tool.isComposio}
/> />
</div>
} }
/> />
</div>
))} ))}
</div> </div>
)} )}
</div> </div>
{/* Auth Modal */}
{hasToolkitWithAuth && (
<ToolkitAuthModal
key={card.slug}
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
toolkitSlug={card.slug}
projectId={projectId}
onComplete={handleAuthComplete}
/>
)}
</>
); );
}; };

View file

@ -0,0 +1,151 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Checkbox } from "@heroui/react";
import { z } from "zod";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import { RefreshCwIcon } from "lucide-react";
import { fetchMcpTools } from "@/app/actions/mcp_actions";
interface McpImportToolsProps {
projectId: string;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onImport: (tools: z.infer<typeof WorkflowTool>[]) => void;
}
export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tools, setTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
const [selectedTools, setSelectedTools] = useState<Set<number>>(new Set());
const process = useCallback(async () => {
setLoading(true);
setError(null);
setSelectedTools(new Set());
try {
const result = await fetchMcpTools(projectId);
setTools(result);
// Select all tools by default
setSelectedTools(new Set(result.map((_, index) => index)));
} catch (error) {
setError(`Unable to fetch tools: ${error}`);
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
console.log("mcp import tools useEffect", isOpen);
if (isOpen) {
process();
}
}, [isOpen, process]);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="xl">
<ModalContent>
{(onClose) => (
<>
<ModalHeader>Import from MCP servers</ModalHeader>
<ModalBody>
{loading && <div className="flex gap-2 items-center">
<Spinner size="sm" />
Fetching tools...
</div>}
{error && <div className="bg-red-100 p-2 rounded-md text-red-800 flex items-center gap-2 text-sm">
{error}
<Button size="sm" color="danger" onPress={() => process()}>Retry</Button>
</div>}
{!loading && !error && <>
<div className="flex items-center justify-between mb-4">
<div className="text-gray-600">
{tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`}
</div>
<Button
size="sm"
variant="flat"
onPress={() => {
setTools([]);
process();
}}
startContent={<RefreshCwIcon className="w-4 h-4" />}
>
Refresh
</Button>
</div>
{tools.length > 0 && <div className="flex flex-col w-full mt-4">
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 rounded-t-lg border-b text-sm text-gray-700 font-medium">
<div className="w-8">
<Checkbox
size="sm"
isSelected={selectedTools.size === tools.length}
isIndeterminate={selectedTools.size > 0 && selectedTools.size < tools.length}
onValueChange={(checked) => {
if (checked) {
setSelectedTools(new Set(tools.map((_, i) => i)));
} else {
setSelectedTools(new Set());
}
}}
/>
</div>
<div className="w-36">Server</div>
<div className="flex-1">Tool Name</div>
</div>
<div className="border rounded-b-lg divide-y overflow-y-auto max-h-[300px]">
{tools.map((t, index) => (
<div
key={index}
className="flex items-center gap-4 px-4 py-2 hover:bg-gray-50 transition-colors"
>
<div className="w-8">
<Checkbox
size="sm"
isSelected={selectedTools.has(index)}
onValueChange={(checked) => {
const newSelected = new Set(selectedTools);
if (checked) {
newSelected.add(index);
} else {
newSelected.delete(index);
}
setSelectedTools(newSelected);
}}
/>
</div>
<div className="w-36">
<div className="bg-blue-50 px-2 py-1 rounded text-blue-700 text-sm font-medium border border-blue-100">
{t.mcpServerName}
</div>
</div>
<div className="flex-1 truncate text-gray-700">{t.name}</div>
</div>
))}
</div>
</div>}
{tools.length > 0 && (
<div className="mt-4 text-sm text-gray-600">
{selectedTools.size} of {tools.length} tools selected
</div>
)}
</>}
</ModalBody>
<ModalFooter>
<Button size="sm" variant="flat" onPress={onClose}>
Cancel
</Button>
{tools.length > 0 && <Button size="sm" onPress={() => {
const selectedToolsList = tools.filter((_, index) => selectedTools.has(index));
onImport(selectedToolsList);
onClose();
}}>
Import
</Button>}
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
}

View file

@ -3,6 +3,7 @@ import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, c
import { MCPServer, Message, WithStringId } from "../../../lib/types/types"; import { MCPServer, Message, WithStringId } from "../../../lib/types/types";
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types"; import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types";
import { DataSource } from "../../../lib/types/datasource_types"; import { DataSource } from "../../../lib/types/datasource_types";
import { Project } from "../../../lib/types/project_types";
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer'; import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
import { AgentConfig } from "../entities/agent_config"; import { AgentConfig } from "../entities/agent_config";
import { ToolConfig } from "../entities/tool_config"; import { ToolConfig } from "../entities/tool_config";
@ -142,6 +143,9 @@ export type Action = {
type: "show_visualise"; type: "show_visualise";
} | { } | {
type: "hide_visualise"; type: "hide_visualise";
} | {
type: "sync_workflow";
workflow: z.infer<typeof Workflow>;
}; };
function reducer(state: State, action: Action): State { function reducer(state: State, action: Action): State {
@ -207,6 +211,13 @@ function reducer(state: State, action: Action): State {
}); });
break; break;
} }
case "sync_workflow": {
newState = produce(state, draft => {
draft.present.workflow = action.workflow;
draft.present.lastUpdatedAt = action.workflow.lastUpdatedAt;
});
break;
}
case "reorder_agents": { case "reorder_agents": {
const newState = produce(state.present, draft => { const newState = produce(state.present, draft => {
draft.workflow.agents = action.agents; draft.workflow.agents = action.agents;
@ -579,10 +590,12 @@ export function WorkflowEditor({
toolWebhookUrl, toolWebhookUrl,
defaultModel, defaultModel,
projectTools, projectTools,
projectConfig,
eligibleModels, eligibleModels,
isLive, isLive,
onChangeMode, onChangeMode,
onRevertToLive, onRevertToLive,
onProjectToolsUpdated,
}: { }: {
projectId: string; projectId: string;
dataSources: WithStringId<z.infer<typeof DataSource>>[]; dataSources: WithStringId<z.infer<typeof DataSource>>[];
@ -592,10 +605,12 @@ export function WorkflowEditor({
toolWebhookUrl: string; toolWebhookUrl: string;
defaultModel: string; defaultModel: string;
projectTools: z.infer<typeof WorkflowTool>[]; projectTools: z.infer<typeof WorkflowTool>[];
projectConfig: z.infer<typeof Project>;
eligibleModels: z.infer<typeof ModelsResponse> | "*"; eligibleModels: z.infer<typeof ModelsResponse> | "*";
isLive: boolean; isLive: boolean;
onChangeMode: (mode: 'draft' | 'live') => void; onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void; onRevertToLive: () => void;
onProjectToolsUpdated?: () => void;
}) { }) {
const [state, dispatch] = useReducer(reducer, { const [state, dispatch] = useReducer(reducer, {
@ -615,6 +630,12 @@ export function WorkflowEditor({
isLive, isLive,
} }
}); });
// Sync workflow prop changes with reducer state (e.g., when composio tools are updated)
useEffect(() => {
dispatch({ type: "sync_workflow", workflow });
}, [workflow]);
const [chatMessages, setChatMessages] = useState<z.infer<typeof Message>[]>([]); const [chatMessages, setChatMessages] = useState<z.infer<typeof Message>[]>([]);
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => { const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
setChatMessages(messages); setChatMessages(messages);
@ -980,6 +1001,7 @@ export function WorkflowEditor({
tools={state.present.workflow.tools} tools={state.present.workflow.tools}
projectTools={projectTools} projectTools={projectTools}
prompts={state.present.workflow.prompts} prompts={state.present.workflow.prompts}
workflow={state.present.workflow}
selectedEntity={ selectedEntity={
state.present.selection && state.present.selection &&
(state.present.selection.type === "agent" || (state.present.selection.type === "agent" ||
@ -1002,6 +1024,8 @@ export function WorkflowEditor({
onDeletePrompt={handleDeletePrompt} onDeletePrompt={handleDeletePrompt}
onShowVisualise={handleShowVisualise} onShowVisualise={handleShowVisualise}
projectId={projectId} projectId={projectId}
onProjectToolsUpdated={onProjectToolsUpdated}
projectConfig={projectConfig}
onReorderAgents={handleReorderAgents} onReorderAgents={handleReorderAgents}
/> />
</div> </div>

View file

@ -14,8 +14,7 @@ import {
ChevronRightIcon, ChevronRightIcon,
Moon, Moon,
Sun, Sun,
HelpCircle, HelpCircle
Wrench
} from "lucide-react"; } from "lucide-react";
import { getProjectConfig } from "@/app/actions/project_actions"; import { getProjectConfig } from "@/app/actions/project_actions";
import { useTheme } from "@/app/providers/theme-provider"; import { useTheme } from "@/app/providers/theme-provider";
@ -69,13 +68,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
icon: DatabaseIcon, icon: DatabaseIcon,
requiresProject: true requiresProject: true
}] : []), }] : []),
{
href: 'tools',
label: 'Tools',
icon: Wrench,
requiresProject: true,
beta: true
},
{ {
href: 'config', href: 'config',
label: 'Settings', label: 'Settings',
@ -162,14 +154,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
`} `}
/> />
{!collapsed && ( {!collapsed && (
<>
<span>{item.label}</span> <span>{item.label}</span>
{item.beta && (
<span className="ml-1.5 leading-none px-1.5 py-[2px] text-[9px] font-medium bg-linear-to-r from-pink-500 to-violet-500 text-white rounded-full">
BETA
</span>
)}
</>
)} )}
</Link> </Link>
</Tooltip> </Tooltip>

View file

@ -43,7 +43,7 @@
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"jose": "^5.9.6", "jose": "^5.9.6",
"lucide-react": "^0.465.0", "lucide-react": "^0.465.0",
"mermaid": "^11.8.1", "mermaid": "^11.9.0",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"next": "15.3.4", "next": "15.3.4",
"openai": "^4.67.2", "openai": "^4.67.2",
@ -1396,7 +1396,6 @@
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@chevrotain/gast": "11.0.3", "@chevrotain/gast": "11.0.3",
"@chevrotain/types": "11.0.3", "@chevrotain/types": "11.0.3",
@ -1407,7 +1406,6 @@
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@chevrotain/types": "11.0.3", "@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21" "lodash-es": "4.17.21"
@ -1416,20 +1414,17 @@
"node_modules/@chevrotain/regexp-to-ast": { "node_modules/@chevrotain/regexp-to-ast": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="
"license": "Apache-2.0"
}, },
"node_modules/@chevrotain/types": { "node_modules/@chevrotain/types": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="
"license": "Apache-2.0"
}, },
"node_modules/@chevrotain/utils": { "node_modules/@chevrotain/utils": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="
"license": "Apache-2.0"
}, },
"node_modules/@composio/client": { "node_modules/@composio/client": {
"version": "0.1.0-alpha.26", "version": "0.1.0-alpha.26",
@ -4582,10 +4577,9 @@
} }
}, },
"node_modules/@mermaid-js/parser": { "node_modules/@mermaid-js/parser": {
"version": "0.6.1", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz", "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
"integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==", "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"langium": "3.3.1" "langium": "3.3.1"
} }
@ -9222,7 +9216,6 @@
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3", "@chevrotain/gast": "11.0.3",
@ -9236,7 +9229,6 @@
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
"license": "MIT",
"dependencies": { "dependencies": {
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
@ -12928,7 +12920,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
"license": "MIT",
"dependencies": { "dependencies": {
"chevrotain": "~11.0.3", "chevrotain": "~11.0.3",
"chevrotain-allstar": "~0.3.0", "chevrotain-allstar": "~0.3.0",
@ -13440,15 +13431,14 @@
} }
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "15.0.12", "version": "16.1.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.0.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "integrity": "sha512-Me7BNa1aqrxVinDnFfvCgHh2yHvLbFvILBs899MhuBpbE5VPzpSqv7alaESfkqkgc9JNvUGH4gqwZeOzLnY8Jg==",
"license": "MIT",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
@ -13781,14 +13771,13 @@
} }
}, },
"node_modules/mermaid": { "node_modules/mermaid": {
"version": "11.8.1", "version": "11.9.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz",
"integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==", "integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==",
"license": "MIT",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^7.0.4", "@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^2.1.33", "@iconify/utils": "^2.1.33",
"@mermaid-js/parser": "^0.6.1", "@mermaid-js/parser": "^0.6.2",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"cytoscape": "^3.29.3", "cytoscape": "^3.29.3",
"cytoscape-cose-bilkent": "^4.1.0", "cytoscape-cose-bilkent": "^4.1.0",
@ -13798,10 +13787,10 @@
"dagre-d3-es": "7.0.11", "dagre-d3-es": "7.0.11",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.5", "dompurify": "^3.2.5",
"katex": "^0.16.9", "katex": "^0.16.22",
"khroma": "^2.1.0", "khroma": "^2.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.7", "marked": "^16.0.0",
"roughjs": "^4.6.6", "roughjs": "^4.6.6",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"ts-dedent": "^2.2.0", "ts-dedent": "^2.2.0",
@ -17797,7 +17786,6 @@
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@ -17806,7 +17794,6 @@
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
"license": "MIT",
"dependencies": { "dependencies": {
"vscode-languageserver-protocol": "3.17.5" "vscode-languageserver-protocol": "3.17.5"
}, },
@ -17818,7 +17805,6 @@
"version": "3.17.5", "version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": { "dependencies": {
"vscode-jsonrpc": "8.2.0", "vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5" "vscode-languageserver-types": "3.17.5"
@ -17827,20 +17813,17 @@
"node_modules/vscode-languageserver-textdocument": { "node_modules/vscode-languageserver-textdocument": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="
"license": "MIT"
}, },
"node_modules/vscode-languageserver-types": { "node_modules/vscode-languageserver-types": {
"version": "3.17.5", "version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
"license": "MIT"
}, },
"node_modules/vscode-uri": { "node_modules/vscode-uri": {
"version": "3.0.8", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="
"license": "MIT"
}, },
"node_modules/web-streams-polyfill": { "node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3", "version": "4.0.0-beta.3",

View file

@ -50,7 +50,7 @@
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"jose": "^5.9.6", "jose": "^5.9.6",
"lucide-react": "^0.465.0", "lucide-react": "^0.465.0",
"mermaid": "^11.8.1", "mermaid": "^11.9.0",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"next": "15.3.4", "next": "15.3.4",
"openai": "^4.67.2", "openai": "^4.67.2",