From 22abf988357cbe8525d53f123fc092df13a2df2b Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 9 Dec 2025 12:44:53 -0800 Subject: [PATCH 01/14] fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead (#2273) * fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead * added backfill, added loading state for tools in settings > mcp * fix tool inp --- apps/sim/app/api/mcp/tools/execute/route.ts | 73 ++- .../components/tool-input/tool-input.tsx | 50 ++- .../server-list-item/server-list-item.tsx | 6 +- .../settings-modal/components/mcp/mcp.tsx | 9 +- .../handlers/agent/agent-handler.test.ts | 334 ++++++++++++++ .../executor/handlers/agent/agent-handler.ts | 414 +++++++++++++----- apps/sim/lib/mcp/service.ts | 114 +++-- 7 files changed, 814 insertions(+), 186 deletions(-) diff --git a/apps/sim/app/api/mcp/tools/execute/route.ts b/apps/sim/app/api/mcp/tools/execute/route.ts index 48205811d4..d58d0bea24 100644 --- a/apps/sim/app/api/mcp/tools/execute/route.ts +++ b/apps/sim/app/api/mcp/tools/execute/route.ts @@ -15,7 +15,6 @@ const logger = createLogger('McpToolExecutionAPI') export const dynamic = 'force-dynamic' -// Type definitions for improved type safety interface SchemaProperty { type: 'string' | 'number' | 'boolean' | 'object' | 'array' description?: string @@ -31,9 +30,6 @@ interface ToolExecutionResult { error?: string } -/** - * Type guard to safely check if a schema property has a type field - */ function hasType(prop: unknown): prop is SchemaProperty { return typeof prop === 'object' && prop !== null && 'type' in prop } @@ -57,7 +53,8 @@ export const POST = withMcpAuth('read')( userId: userId, }) - const { serverId, toolName, arguments: args } = body + const { serverId, toolName, arguments: rawArgs } = body + const args = rawArgs || {} const serverIdValidation = validateStringParam(serverId, 'serverId') if (!serverIdValidation.isValid) { @@ -75,22 +72,31 @@ export const POST = withMcpAuth('read')( `[${requestId}] Executing tool ${toolName} on server ${serverId} for user ${userId} in workspace ${workspaceId}` ) - let tool = null + let tool: McpTool | null = null try { - const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId) - tool = tools.find((t) => t.name === toolName) - - if (!tool) { - return createMcpErrorResponse( - new Error( - `Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}` - ), - 'Tool not found', - 404 - ) + if (body.toolSchema) { + tool = { + name: toolName, + inputSchema: body.toolSchema, + serverId: serverId, + serverName: 'provided-schema', + } as McpTool + logger.debug(`[${requestId}] Using provided schema for ${toolName}, skipping discovery`) + } else { + const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId) + tool = tools.find((t) => t.name === toolName) ?? null + + if (!tool) { + return createMcpErrorResponse( + new Error( + `Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}` + ), + 'Tool not found', + 404 + ) + } } - // Cast arguments to their expected types based on tool schema if (tool.inputSchema?.properties) { for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) { const schema = paramSchema as any @@ -100,7 +106,6 @@ export const POST = withMcpAuth('read')( continue } - // Cast numbers if ( (schema.type === 'number' || schema.type === 'integer') && typeof value === 'string' @@ -110,42 +115,33 @@ export const POST = withMcpAuth('read')( if (!Number.isNaN(numValue)) { args[paramName] = numValue } - } - // Cast booleans - else if (schema.type === 'boolean' && typeof value === 'string') { + } else if (schema.type === 'boolean' && typeof value === 'string') { if (value.toLowerCase() === 'true') { args[paramName] = true } else if (value.toLowerCase() === 'false') { args[paramName] = false } - } - // Cast arrays - else if (schema.type === 'array' && typeof value === 'string') { + } else if (schema.type === 'array' && typeof value === 'string') { const stringValue = value.trim() if (stringValue) { try { - // Try to parse as JSON first (handles ["item1", "item2"]) const parsed = JSON.parse(stringValue) if (Array.isArray(parsed)) { args[paramName] = parsed } else { - // JSON parsed but not an array, wrap in array args[paramName] = [parsed] } - } catch (error) { - // JSON parsing failed - treat as comma-separated if contains commas, otherwise single item + } catch { if (stringValue.includes(',')) { args[paramName] = stringValue .split(',') .map((item) => item.trim()) .filter((item) => item) } else { - // Single item - wrap in array since schema expects array args[paramName] = [stringValue] } } } else { - // Empty string becomes empty array args[paramName] = [] } } @@ -172,7 +168,7 @@ export const POST = withMcpAuth('read')( const toolCall: McpToolCall = { name: toolName, - arguments: args || {}, + arguments: args, } const result = await Promise.race([ @@ -197,7 +193,6 @@ export const POST = withMcpAuth('read')( } logger.info(`[${requestId}] Successfully executed tool ${toolName} on server ${serverId}`) - // Track MCP tool execution try { const { trackPlatformEvent } = await import('@/lib/core/telemetry') trackPlatformEvent('platform.mcp.tool_executed', { @@ -206,8 +201,8 @@ export const POST = withMcpAuth('read')( 'mcp.execution_status': 'success', 'workspace.id': workspaceId, }) - } catch (_e) { - // Silently fail + } catch { + // Telemetry failure is non-critical } return createMcpSuccessResponse(transformedResult) @@ -220,12 +215,9 @@ export const POST = withMcpAuth('read')( } ) -/** - * Validate tool arguments against schema - */ function validateToolArguments(tool: McpTool, args: Record): string | null { if (!tool.inputSchema) { - return null // No schema to validate against + return null } const schema = tool.inputSchema @@ -270,9 +262,6 @@ function validateToolArguments(tool: McpTool, args: Record): st return null } -/** - * Transform MCP tool result to platform format - */ function transformToolResult(result: McpToolResult): ToolExecutionResult { if (result.isError) { return { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index f925c04c37..c68e7b13d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Loader2, PlusIcon, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' @@ -845,6 +845,52 @@ export function ToolInput({ ? (value as unknown as StoredTool[]) : [] + const hasBackfilledRef = useRef(false) + useEffect(() => { + if ( + isPreview || + mcpLoading || + mcpTools.length === 0 || + selectedTools.length === 0 || + hasBackfilledRef.current + ) { + return + } + + const mcpToolsNeedingSchema = selectedTools.filter( + (tool) => tool.type === 'mcp' && !tool.schema && tool.params?.toolName + ) + + if (mcpToolsNeedingSchema.length === 0) { + return + } + + const updatedTools = selectedTools.map((tool) => { + if (tool.type !== 'mcp' || tool.schema || !tool.params?.toolName) { + return tool + } + + const mcpTool = mcpTools.find( + (mt) => mt.name === tool.params?.toolName && mt.serverId === tool.params?.serverId + ) + + if (mcpTool?.inputSchema) { + logger.info(`Backfilling schema for MCP tool: ${tool.params.toolName}`) + return { ...tool, schema: mcpTool.inputSchema } + } + + return tool + }) + + const hasChanges = updatedTools.some((tool, i) => tool.schema && !selectedTools[i].schema) + + if (hasChanges) { + hasBackfilledRef.current = true + logger.info(`Backfilled schemas for ${mcpToolsNeedingSchema.length} MCP tool(s)`) + setStoreValue(updatedTools) + } + }, [mcpTools, mcpLoading, selectedTools, isPreview, setStoreValue]) + /** * Checks if a tool is already selected in the current workflow * @param toolId - The tool identifier to check @@ -2314,7 +2360,7 @@ export function ToolInput({ mcpTools={mcpTools} searchQuery={searchQuery || ''} customFilter={customFilter} - onToolSelect={(tool) => handleMcpToolSelect(tool, false)} + onToolSelect={handleMcpToolSelect} disabled={false} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx index a4de645a8f..e35a82f64e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx @@ -28,6 +28,7 @@ interface ServerListItemProps { server: any tools: any[] isDeleting: boolean + isLoadingTools?: boolean onRemove: () => void onViewDetails: () => void } @@ -39,6 +40,7 @@ export function ServerListItem({ server, tools, isDeleting, + isLoadingTools = false, onRemove, onViewDetails, }: ServerListItemProps) { @@ -54,7 +56,9 @@ export function ServerListItem({ ({transportLabel}) -

{toolsLabel}

+

+ {isLoadingTools && tools.length === 0 ? 'Loading...' : toolsLabel} +

) diff --git a/apps/sim/lib/workflows/autolayout/constants.ts b/apps/sim/lib/workflows/autolayout/constants.ts index c932ede0a0..7616fb0944 100644 --- a/apps/sim/lib/workflows/autolayout/constants.ts +++ b/apps/sim/lib/workflows/autolayout/constants.ts @@ -75,7 +75,6 @@ export const DEFAULT_LAYOUT_OPTIONS = { horizontalSpacing: DEFAULT_HORIZONTAL_SPACING, verticalSpacing: DEFAULT_VERTICAL_SPACING, padding: DEFAULT_LAYOUT_PADDING, - alignment: 'center' as const, } /** @@ -90,5 +89,4 @@ export const CONTAINER_LAYOUT_OPTIONS = { horizontalSpacing: DEFAULT_CONTAINER_HORIZONTAL_SPACING, verticalSpacing: DEFAULT_VERTICAL_SPACING, padding: { x: CONTAINER_PADDING_X, y: CONTAINER_PADDING_Y }, - alignment: 'center' as const, } diff --git a/apps/sim/lib/workflows/autolayout/containers.ts b/apps/sim/lib/workflows/autolayout/containers.ts index 53cfd0c0bc..cdd79fcadc 100644 --- a/apps/sim/lib/workflows/autolayout/containers.ts +++ b/apps/sim/lib/workflows/autolayout/containers.ts @@ -28,16 +28,12 @@ export function layoutContainers( ): void { const { children } = getBlocksByParent(blocks) - // Build container-specific layout options - // If horizontalSpacing provided, reduce by 15% for tighter container layout - // Otherwise use the default container spacing (400) const containerOptions: LayoutOptions = { horizontalSpacing: options.horizontalSpacing ? options.horizontalSpacing * 0.85 : DEFAULT_CONTAINER_HORIZONTAL_SPACING, verticalSpacing: options.verticalSpacing ?? DEFAULT_VERTICAL_SPACING, padding: { x: CONTAINER_PADDING_X, y: CONTAINER_PADDING_Y }, - alignment: options.alignment, } for (const [parentId, childIds] of children.entries()) { diff --git a/apps/sim/lib/workflows/autolayout/core.ts b/apps/sim/lib/workflows/autolayout/core.ts index 0131984756..745b4865ef 100644 --- a/apps/sim/lib/workflows/autolayout/core.ts +++ b/apps/sim/lib/workflows/autolayout/core.ts @@ -10,12 +10,55 @@ import { normalizePositions, prepareBlockMetrics, } from '@/lib/workflows/autolayout/utils' +import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' import type { BlockState } from '@/stores/workflows/workflow/types' const logger = createLogger('AutoLayout:Core') -/** Handle names that indicate edges from subflow end */ const SUBFLOW_END_HANDLES = new Set(['loop-end-source', 'parallel-end-source']) +const SUBFLOW_START_HANDLES = new Set(['loop-start-source', 'parallel-start-source']) + +/** + * Calculates the Y offset for a source handle based on block type and handle ID. + */ +function getSourceHandleYOffset(block: BlockState, sourceHandle?: string | null): number { + if (sourceHandle === 'error') { + const blockHeight = block.height || BLOCK_DIMENSIONS.MIN_HEIGHT + return blockHeight - HANDLE_POSITIONS.ERROR_BOTTOM_OFFSET + } + + if (sourceHandle && SUBFLOW_START_HANDLES.has(sourceHandle)) { + return HANDLE_POSITIONS.SUBFLOW_START_Y_OFFSET + } + + if (block.type === 'condition' && sourceHandle?.startsWith('condition-')) { + const conditionId = sourceHandle.replace('condition-', '') + try { + const conditionsValue = block.subBlocks?.conditions?.value + if (typeof conditionsValue === 'string' && conditionsValue) { + const conditions = JSON.parse(conditionsValue) as Array<{ id?: string }> + const conditionIndex = conditions.findIndex((c) => c.id === conditionId) + if (conditionIndex >= 0) { + return ( + HANDLE_POSITIONS.CONDITION_START_Y + + conditionIndex * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT + ) + } + } + } catch { + // Fall back to default offset + } + } + + return HANDLE_POSITIONS.DEFAULT_Y_OFFSET +} + +/** + * Calculates the Y offset for a target handle based on block type and handle ID. + */ +function getTargetHandleYOffset(_block: BlockState, _targetHandle?: string | null): number { + return HANDLE_POSITIONS.DEFAULT_Y_OFFSET +} /** * Checks if an edge comes from a subflow end handle @@ -225,18 +268,36 @@ function resolveVerticalOverlaps(nodes: GraphNode[], verticalSpacing: number): v } } +/** + * Checks if a block is a container type (loop or parallel) + */ +function isContainerBlock(node: GraphNode): boolean { + return node.block.type === 'loop' || node.block.type === 'parallel' +} + +/** + * Extra vertical spacing after containers to prevent edge crossings with sibling blocks. + * This creates clearance for edges from container ends to route cleanly. + */ +const CONTAINER_VERTICAL_CLEARANCE = 120 + /** * Calculates positions for nodes organized by layer. * Uses cumulative width-based X positioning to properly handle containers of varying widths. + * Aligns blocks based on their connected predecessors to achieve handle-to-handle alignment. + * + * Handle alignment: Calculates actual source handle Y positions based on block type + * (condition blocks have handles at different heights for each branch). + * Target handles are also calculated per-block to ensure precise alignment. */ export function calculatePositions( layers: Map, + edges: Edge[], options: LayoutOptions = {} ): void { const horizontalSpacing = options.horizontalSpacing ?? DEFAULT_LAYOUT_OPTIONS.horizontalSpacing const verticalSpacing = options.verticalSpacing ?? DEFAULT_LAYOUT_OPTIONS.verticalSpacing const padding = options.padding ?? DEFAULT_LAYOUT_OPTIONS.padding - const alignment = options.alignment ?? DEFAULT_LAYOUT_OPTIONS.alignment const layerNumbers = Array.from(layers.keys()).sort((a, b) => a - b) @@ -257,41 +318,89 @@ export function calculatePositions( cumulativeX += layerWidths.get(layerNum)! + horizontalSpacing } - // Position nodes using cumulative X + // Build a flat map of all nodes for quick lookups + const allNodes = new Map() + for (const nodesInLayer of layers.values()) { + for (const node of nodesInLayer) { + allNodes.set(node.id, node) + } + } + + // Build incoming edges map for handle lookups + const incomingEdgesMap = new Map() + for (const edge of edges) { + if (!incomingEdgesMap.has(edge.target)) { + incomingEdgesMap.set(edge.target, []) + } + incomingEdgesMap.get(edge.target)!.push(edge) + } + + // Position nodes layer by layer, aligning with connected predecessors for (const layerNum of layerNumbers) { const nodesInLayer = layers.get(layerNum)! const xPosition = layerXPositions.get(layerNum)! - // Calculate total height for this layer - const totalHeight = nodesInLayer.reduce( - (sum, node, idx) => sum + node.metrics.height + (idx > 0 ? verticalSpacing : 0), - 0 - ) - - // Start Y based on alignment - let yOffset: number - switch (alignment) { - case 'start': - yOffset = padding.y - break - case 'center': - yOffset = Math.max(padding.y, 300 - totalHeight / 2) - break - case 'end': - yOffset = 600 - totalHeight - padding.y - break - default: - yOffset = padding.y - break + // Separate containers and non-containers + const containersInLayer = nodesInLayer.filter(isContainerBlock) + const nonContainersInLayer = nodesInLayer.filter((n) => !isContainerBlock(n)) + + // For the first layer (layer 0), position sequentially from padding.y + if (layerNum === 0) { + let yOffset = padding.y + + // Sort containers by height for visual balance + containersInLayer.sort((a, b) => b.metrics.height - a.metrics.height) + + for (const node of containersInLayer) { + node.position = { x: xPosition, y: yOffset } + yOffset += node.metrics.height + verticalSpacing + } + + if (containersInLayer.length > 0 && nonContainersInLayer.length > 0) { + yOffset += CONTAINER_VERTICAL_CLEARANCE + } + + // Sort non-containers by outgoing connections + nonContainersInLayer.sort((a, b) => b.outgoing.size - a.outgoing.size) + + for (const node of nonContainersInLayer) { + node.position = { x: xPosition, y: yOffset } + yOffset += node.metrics.height + verticalSpacing + } + continue } - // Position each node - for (const node of nodesInLayer) { - node.position = { - x: xPosition, - y: yOffset, + // For subsequent layers, align with connected predecessors (handle-to-handle) + for (const node of [...containersInLayer, ...nonContainersInLayer]) { + // Find the bottommost predecessor handle Y (highest value) and align to it + let bestSourceHandleY = -1 + let bestEdge: Edge | null = null + const incomingEdges = incomingEdgesMap.get(node.id) || [] + + for (const edge of incomingEdges) { + const predecessor = allNodes.get(edge.source) + if (predecessor) { + // Calculate actual source handle Y position based on block type and handle + const sourceHandleOffset = getSourceHandleYOffset(predecessor.block, edge.sourceHandle) + const sourceHandleY = predecessor.position.y + sourceHandleOffset + + if (sourceHandleY > bestSourceHandleY) { + bestSourceHandleY = sourceHandleY + bestEdge = edge + } + } + } + + // If no predecessors found (shouldn't happen for layer > 0), use padding + if (bestSourceHandleY < 0) { + bestSourceHandleY = padding.y + HANDLE_POSITIONS.DEFAULT_Y_OFFSET } - yOffset += node.metrics.height + verticalSpacing + + // Calculate the target handle Y offset for this node + const targetHandleOffset = getTargetHandleYOffset(node.block, bestEdge?.targetHandle) + + // Position node so its target handle aligns with the source handle Y + node.position = { x: xPosition, y: bestSourceHandleY - targetHandleOffset } } } @@ -338,8 +447,8 @@ export function layoutBlocksCore( // 3. Group by layer const layers = groupByLayer(nodes) - // 4. Calculate positions - calculatePositions(layers, layoutOptions) + // 4. Calculate positions (pass edges for handle offset calculations) + calculatePositions(layers, edges, layoutOptions) // 5. Normalize positions const dimensions = normalizePositions(nodes, { isContainer: options.isContainer }) diff --git a/apps/sim/lib/workflows/autolayout/targeted.ts b/apps/sim/lib/workflows/autolayout/targeted.ts index 97cb9e0715..f4b741bd8f 100644 --- a/apps/sim/lib/workflows/autolayout/targeted.ts +++ b/apps/sim/lib/workflows/autolayout/targeted.ts @@ -228,7 +228,6 @@ function computeLayoutPositions( layoutOptions: { horizontalSpacing: isContainer ? horizontalSpacing * 0.85 : horizontalSpacing, verticalSpacing, - alignment: 'center', }, subflowDepths, }) diff --git a/apps/sim/lib/workflows/autolayout/types.ts b/apps/sim/lib/workflows/autolayout/types.ts index ed763ae571..a20c35715a 100644 --- a/apps/sim/lib/workflows/autolayout/types.ts +++ b/apps/sim/lib/workflows/autolayout/types.ts @@ -4,7 +4,6 @@ export interface LayoutOptions { horizontalSpacing?: number verticalSpacing?: number padding?: { x: number; y: number } - alignment?: 'start' | 'center' | 'end' } export interface LayoutResult { diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index 7c63edc81a..45ddc614a4 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -329,7 +329,6 @@ export type LayoutFunction = ( horizontalSpacing?: number verticalSpacing?: number padding?: { x: number; y: number } - alignment?: 'start' | 'center' | 'end' } subflowDepths?: Map } @@ -418,7 +417,6 @@ export function prepareContainerDimensions( layoutOptions: { horizontalSpacing: horizontalSpacing * 0.85, verticalSpacing, - alignment: 'center', }, }) diff --git a/apps/sim/lib/workflows/blocks/block-dimensions.ts b/apps/sim/lib/workflows/blocks/block-dimensions.ts index cff175dc30..d311f0dc0f 100644 --- a/apps/sim/lib/workflows/blocks/block-dimensions.ts +++ b/apps/sim/lib/workflows/blocks/block-dimensions.ts @@ -2,56 +2,42 @@ * Shared Block Dimension Constants * * Single source of truth for block dimensions used by: - * - UI components (workflow-block, note-block) + * - UI components (workflow-block, note-block, subflow-node) * - Autolayout system * - Node utilities - * - * IMPORTANT: These values must match the actual CSS dimensions in the UI. - * Changing these values will affect both rendering and layout calculations. */ -/** - * Block dimension constants for workflow blocks - */ export const BLOCK_DIMENSIONS = { - /** Fixed width for all workflow blocks (matches w-[250px] in workflow-block.tsx) */ FIXED_WIDTH: 250, - - /** Header height for blocks */ HEADER_HEIGHT: 40, - - /** Minimum height for blocks */ MIN_HEIGHT: 100, - - /** Padding around workflow block content (p-[8px] top + bottom = 16px) */ WORKFLOW_CONTENT_PADDING: 16, - - /** Height of each subblock row (14px text + 8px gap + padding) */ WORKFLOW_ROW_HEIGHT: 29, - - /** Padding around note block content */ NOTE_CONTENT_PADDING: 14, - - /** Minimum content height for note blocks */ NOTE_MIN_CONTENT_HEIGHT: 20, - - /** Base content height for note blocks */ NOTE_BASE_CONTENT_HEIGHT: 60, } as const -/** - * Container block dimension constants (loop, parallel, subflow) - */ export const CONTAINER_DIMENSIONS = { - /** Default width for container blocks */ DEFAULT_WIDTH: 500, - - /** Default height for container blocks */ DEFAULT_HEIGHT: 300, - - /** Minimum width for container blocks */ MIN_WIDTH: 400, - - /** Minimum height for container blocks */ MIN_HEIGHT: 200, + HEADER_HEIGHT: 50, +} as const + +/** + * Handle position constants - must match CSS in workflow-block.tsx and subflow-node.tsx + */ +export const HANDLE_POSITIONS = { + /** Default Y offset from block top for source/target handles */ + DEFAULT_Y_OFFSET: 20, + /** Error handle offset from block bottom */ + ERROR_BOTTOM_OFFSET: 17, + /** Condition handle starting Y offset */ + CONDITION_START_Y: 60, + /** Height per condition row */ + CONDITION_ROW_HEIGHT: 29, + /** Subflow start handle Y offset (header 50px + pill offset 16px + pill center 14px) */ + SUBFLOW_START_Y_OFFSET: 80, } as const From c5b3fcb181b853f1bac8a5593f320dded97a0539 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:42:17 -0800 Subject: [PATCH 07/14] fix(copilot): fix custom tools (#2278) * Fix title custom tool * Checkpoitn (broken) * Fix custom tool flash * Edit workflow returns null fix * Works * Fix lint --- .../hooks/use-workflow-execution.ts | 3 +- apps/sim/executor/utils/start-block.ts | 2 +- apps/sim/lib/copilot/registry.ts | 45 ++- .../tools/client/workflow/edit-workflow.ts | 169 +++++---- .../client/workflow/manage-custom-tool.ts | 54 ++- .../tools/client/workflow/manage-mcp-tool.ts | 340 ++++++++++++++++++ apps/sim/stores/panel/copilot/store.ts | 29 ++ bun.lock | 1 - 8 files changed, 532 insertions(+), 111 deletions(-) create mode 100644 apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index c0e8ebd683..c8d5b62f21 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -16,6 +16,7 @@ import { } from '@/lib/workflows/triggers/triggers' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' +import { coerceValue } from '@/executor/utils/start-block' import { subscriptionKeys } from '@/hooks/queries/subscription' import { useExecutionStream } from '@/hooks/use-execution-stream' import { WorkflowValidationError } from '@/serializer' @@ -757,7 +758,7 @@ export function useWorkflowExecution() { if (Array.isArray(inputFormatValue)) { inputFormatValue.forEach((field: any) => { if (field && typeof field === 'object' && field.name && field.value !== undefined) { - testInput[field.name] = field.value + testInput[field.name] = coerceValue(field.type, field.value) } }) } diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index 279d390047..f6c9753bb8 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -132,7 +132,7 @@ function extractInputFormat(block: SerializedBlock): InputFormatField[] { .map((field) => field) } -function coerceValue(type: string | null | undefined, value: unknown): unknown { +export function coerceValue(type: string | null | undefined, value: unknown): unknown { if (value === undefined || value === null) { return value } diff --git a/apps/sim/lib/copilot/registry.ts b/apps/sim/lib/copilot/registry.ts index 1c385588db..67a253cc56 100644 --- a/apps/sim/lib/copilot/registry.ts +++ b/apps/sim/lib/copilot/registry.ts @@ -32,6 +32,7 @@ export const ToolIds = z.enum([ 'navigate_ui', 'knowledge_base', 'manage_custom_tool', + 'manage_mcp_tool', ]) export type ToolId = z.infer @@ -199,12 +200,6 @@ export const ToolArgSchemas = { .describe( 'Required for edit and delete operations. The database ID of the custom tool (e.g., "0robnW7_JUVwZrDkq1mqj"). Use get_workflow_data with data_type "custom_tools" to get the list of tools and their IDs. Do NOT use the function name - use the actual "id" field from the tool.' ), - title: z - .string() - .optional() - .describe( - 'The display title of the custom tool. Required for add. Should always be provided for edit/delete so the user knows which tool is being modified.' - ), schema: z .object({ type: z.literal('function'), @@ -227,6 +222,36 @@ export const ToolArgSchemas = { 'Required for add. The JavaScript function body code. Use {{ENV_VAR}} for environment variables and reference parameters directly by name.' ), }), + + manage_mcp_tool: z.object({ + operation: z + .enum(['add', 'edit', 'delete']) + .describe('The operation to perform: add (create new), edit (update existing), or delete'), + serverId: z + .string() + .optional() + .describe( + 'Required for edit and delete operations. The database ID of the MCP server. Use the MCP settings panel or API to get server IDs.' + ), + config: z + .object({ + name: z.string().describe('The display name for the MCP server'), + transport: z + .enum(['streamable-http']) + .optional() + .default('streamable-http') + .describe('Transport protocol (currently only streamable-http is supported)'), + url: z.string().optional().describe('The MCP server endpoint URL (required for add)'), + headers: z + .record(z.string()) + .optional() + .describe('Optional HTTP headers to send with requests'), + timeout: z.number().optional().describe('Request timeout in milliseconds (default: 30000)'), + enabled: z.boolean().optional().describe('Whether the server is enabled (default: true)'), + }) + .optional() + .describe('Required for add and edit operations. The MCP server configuration.'), + }), } as const export type ToolArgSchemaMap = typeof ToolArgSchemas @@ -292,6 +317,7 @@ export const ToolSSESchemas = { navigate_ui: toolCallSSEFor('navigate_ui', ToolArgSchemas.navigate_ui), knowledge_base: toolCallSSEFor('knowledge_base', ToolArgSchemas.knowledge_base), manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool), + manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool), } as const export type ToolSSESchemaMap = typeof ToolSSESchemas @@ -519,6 +545,13 @@ export const ToolResultSchemas = { title: z.string().optional(), message: z.string().optional(), }), + manage_mcp_tool: z.object({ + success: z.boolean(), + operation: z.enum(['add', 'edit', 'delete']), + serverId: z.string().optional(), + serverName: z.string().optional(), + message: z.string().optional(), + }), } as const export type ToolResultSchemaMap = typeof ToolResultSchemas diff --git a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts index 3d68efb2da..31e48d9943 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts @@ -93,6 +93,26 @@ export class EditWorkflowClientTool extends BaseClientTool { } } + /** + * Safely get the current workflow JSON sanitized for copilot without throwing. + * Used to ensure we always include workflow state in markComplete. + */ + private getCurrentWorkflowJsonSafe(logger: ReturnType): string | undefined { + try { + const currentState = useWorkflowStore.getState().getWorkflowState() + if (!currentState) { + logger.warn('No current workflow state available') + return undefined + } + return this.getSanitizedWorkflowJson(currentState) + } catch (error) { + logger.warn('Failed to get current workflow JSON safely', { + error: error instanceof Error ? error.message : String(error), + }) + return undefined + } + } + static readonly metadata: BaseClientToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 }, @@ -133,66 +153,16 @@ export class EditWorkflowClientTool extends BaseClientTool { async handleAccept(): Promise { const logger = createLogger('EditWorkflowClientTool') - logger.info('handleAccept called', { - toolCallId: this.toolCallId, - state: this.getState(), - hasResult: this.lastResult !== undefined, - }) - this.setState(ClientToolCallState.success) - - // Read from the workflow store to get the actual state with diff applied - const workflowStore = useWorkflowStore.getState() - const currentState = workflowStore.getWorkflowState() - - // Get the workflow state that was applied, merge subblocks, and sanitize - // This matches what get_user_workflow would return - const workflowJson = this.getSanitizedWorkflowJson(currentState) - - // Build sanitized data including workflow JSON and any skipped/validation info from the result - const sanitizedData: Record = {} - if (workflowJson) { - sanitizedData.userWorkflow = workflowJson - } - - // Include skipped items and validation errors in the accept response for LLM feedback - if (this.lastResult?.skippedItems?.length > 0) { - sanitizedData.skippedItems = this.lastResult.skippedItems - sanitizedData.skippedItemsMessage = this.lastResult.skippedItemsMessage - } - if (this.lastResult?.inputValidationErrors?.length > 0) { - sanitizedData.inputValidationErrors = this.lastResult.inputValidationErrors - sanitizedData.inputValidationMessage = this.lastResult.inputValidationMessage - } - - // Build a message that includes info about skipped items - let acceptMessage = 'Workflow edits accepted' - if ( - this.lastResult?.skippedItems?.length > 0 || - this.lastResult?.inputValidationErrors?.length > 0 - ) { - const parts: string[] = [] - if (this.lastResult?.skippedItems?.length > 0) { - parts.push(`${this.lastResult.skippedItems.length} operation(s) were skipped`) - } - if (this.lastResult?.inputValidationErrors?.length > 0) { - parts.push(`${this.lastResult.inputValidationErrors.length} input(s) were rejected`) - } - acceptMessage = `Workflow edits accepted. Note: ${parts.join(', ')}.` - } - - await this.markToolComplete( - 200, - acceptMessage, - Object.keys(sanitizedData).length > 0 ? sanitizedData : undefined - ) + logger.info('handleAccept called', { toolCallId: this.toolCallId, state: this.getState() }) + // Tool was already marked complete in execute() - this is just for UI state this.setState(ClientToolCallState.success) } async handleReject(): Promise { const logger = createLogger('EditWorkflowClientTool') logger.info('handleReject called', { toolCallId: this.toolCallId, state: this.getState() }) + // Tool was already marked complete in execute() - this is just for UI state this.setState(ClientToolCallState.rejected) - await this.markToolComplete(200, 'Workflow changes rejected') } async execute(args?: EditWorkflowArgs): Promise { @@ -202,9 +172,14 @@ export class EditWorkflowClientTool extends BaseClientTool { await this.executeWithTimeout(async () => { if (this.hasExecuted) { logger.info('execute skipped (already executed)', { toolCallId: this.toolCallId }) - // Even if skipped, ensure we mark complete + // Even if skipped, ensure we mark complete with current workflow state if (!this.hasBeenMarkedComplete()) { - await this.markToolComplete(200, 'Tool already executed') + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + await this.markToolComplete( + 200, + 'Tool already executed', + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) } return } @@ -231,7 +206,12 @@ export class EditWorkflowClientTool extends BaseClientTool { const operations = args?.operations || [] if (!operations.length) { this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'No operations provided for edit_workflow') + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + await this.markToolComplete( + 400, + 'No operations provided for edit_workflow', + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) return } @@ -281,12 +261,22 @@ export class EditWorkflowClientTool extends BaseClientTool { if (!res.ok) { const errorText = await res.text().catch(() => '') + let errorMessage: string try { const errorJson = JSON.parse(errorText) - throw new Error(errorJson.error || errorText || `Server error (${res.status})`) + errorMessage = errorJson.error || errorText || `Server error (${res.status})` } catch { - throw new Error(errorText || `Server error (${res.status})`) + errorMessage = errorText || `Server error (${res.status})` } + // Mark complete with error but include current workflow state + this.setState(ClientToolCallState.error) + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + await this.markToolComplete( + res.status, + errorMessage, + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) + return } const json = await res.json() @@ -318,7 +308,14 @@ export class EditWorkflowClientTool extends BaseClientTool { // Update diff directly with workflow state - no YAML conversion needed! if (!result.workflowState) { - throw new Error('No workflow state returned from server') + this.setState(ClientToolCallState.error) + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + await this.markToolComplete( + 500, + 'No workflow state returned from server', + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) + return } let actualDiffWorkflow: WorkflowState | null = null @@ -336,17 +333,37 @@ export class EditWorkflowClientTool extends BaseClientTool { actualDiffWorkflow = workflowStore.getWorkflowState() if (!actualDiffWorkflow) { - throw new Error('Failed to retrieve workflow state after applying changes') + this.setState(ClientToolCallState.error) + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + await this.markToolComplete( + 500, + 'Failed to retrieve workflow state after applying changes', + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) + return } // Get the workflow state that was just applied, merge subblocks, and sanitize // This matches what get_user_workflow would return (the true state after edits were applied) - const workflowJson = this.getSanitizedWorkflowJson(actualDiffWorkflow) + let workflowJson = this.getSanitizedWorkflowJson(actualDiffWorkflow) + + // Fallback: try to get current workflow state if sanitization failed + if (!workflowJson) { + workflowJson = this.getCurrentWorkflowJsonSafe(logger) + } + + // userWorkflow must always be present on success - log error if missing + if (!workflowJson) { + logger.error('Failed to get workflow JSON on success path - this should not happen', { + toolCallId: this.toolCallId, + workflowId: this.workflowId, + }) + } // Build sanitized data including workflow JSON and any skipped/validation info - const sanitizedData: Record = {} - if (workflowJson) { - sanitizedData.userWorkflow = workflowJson + // Always include userWorkflow on success paths + const sanitizedData: Record = { + userWorkflow: workflowJson ?? '{}', // Fallback to empty object JSON if all else fails } // Include skipped items and validation errors in the response for LLM feedback @@ -372,21 +389,25 @@ export class EditWorkflowClientTool extends BaseClientTool { completeMessage = `Workflow diff ready for review. Note: ${parts.join(', ')}.` } - // Mark complete early to unblock LLM stream - await this.markToolComplete( - 200, - completeMessage, - Object.keys(sanitizedData).length > 0 ? sanitizedData : undefined - ) + // Mark complete early to unblock LLM stream - sanitizedData always has userWorkflow + await this.markToolComplete(200, completeMessage, sanitizedData) // Move into review state this.setState(ClientToolCallState.review, { result }) } catch (fetchError: any) { clearTimeout(fetchTimeout) - if (fetchError.name === 'AbortError') { - throw new Error('Server request timed out') - } - throw fetchError + // Handle error with current workflow state + this.setState(ClientToolCallState.error) + const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger) + const errorMessage = + fetchError.name === 'AbortError' + ? 'Server request timed out' + : fetchError.message || String(fetchError) + await this.markToolComplete( + 500, + errorMessage, + currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined + ) } }) } diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts index 9537a709fb..52ef2e68db 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts @@ -25,7 +25,6 @@ interface CustomToolSchema { interface ManageCustomToolArgs { operation: 'add' | 'edit' | 'delete' toolId?: string - title?: string schema?: CustomToolSchema code?: string } @@ -72,12 +71,12 @@ export class ManageCustomToolClientTool extends BaseClientTool { // Return undefined if no operation yet - use static defaults if (!operation) return undefined - // Get tool name from params, or look it up from the store by toolId - let toolName = params?.title || params?.schema?.function?.name + // Get tool name from schema, or look it up from the store by toolId + let toolName = params?.schema?.function?.name if (!toolName && params?.toolId) { try { const tool = useCustomToolsStore.getState().getTool(params.toolId) - toolName = tool?.title || tool?.schema?.function?.name + toolName = tool?.schema?.function?.name } catch { // Ignore errors accessing store } @@ -190,7 +189,7 @@ export class ManageCustomToolClientTool extends BaseClientTool { throw new Error('Operation is required') } - const { operation, toolId, title, schema, code } = args + const { operation, toolId, schema, code } = args // Get workspace ID from the workflow registry const { hydration } = useWorkflowRegistry.getState() @@ -202,16 +201,16 @@ export class ManageCustomToolClientTool extends BaseClientTool { logger.info(`Executing custom tool operation: ${operation}`, { operation, toolId, - title, + functionName: schema?.function?.name, workspaceId, }) switch (operation) { case 'add': - await this.addCustomTool({ title, schema, code, workspaceId }, logger) + await this.addCustomTool({ schema, code, workspaceId }, logger) break case 'edit': - await this.editCustomTool({ toolId, title, schema, code, workspaceId }, logger) + await this.editCustomTool({ toolId, schema, code, workspaceId }, logger) break case 'delete': await this.deleteCustomTool({ toolId, workspaceId }, logger) @@ -226,18 +225,14 @@ export class ManageCustomToolClientTool extends BaseClientTool { */ private async addCustomTool( params: { - title?: string schema?: CustomToolSchema code?: string workspaceId: string }, logger: ReturnType ): Promise { - const { title, schema, code, workspaceId } = params + const { schema, code, workspaceId } = params - if (!title) { - throw new Error('Title is required for adding a custom tool') - } if (!schema) { throw new Error('Schema is required for adding a custom tool') } @@ -245,11 +240,13 @@ export class ManageCustomToolClientTool extends BaseClientTool { throw new Error('Code is required for adding a custom tool') } + const functionName = schema.function.name + const response = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - tools: [{ title, schema, code }], + tools: [{ title: functionName, schema, code }], workspaceId, }), }) @@ -265,14 +262,14 @@ export class ManageCustomToolClientTool extends BaseClientTool { } const createdTool = data.data[0] - logger.info(`Created custom tool: ${title}`, { toolId: createdTool.id }) + logger.info(`Created custom tool: ${functionName}`, { toolId: createdTool.id }) this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Created custom tool "${title}"`, { + await this.markToolComplete(200, `Created custom tool "${functionName}"`, { success: true, operation: 'add', toolId: createdTool.id, - title, + functionName, }) } @@ -282,22 +279,21 @@ export class ManageCustomToolClientTool extends BaseClientTool { private async editCustomTool( params: { toolId?: string - title?: string schema?: CustomToolSchema code?: string workspaceId: string }, logger: ReturnType ): Promise { - const { toolId, title, schema, code, workspaceId } = params + const { toolId, schema, code, workspaceId } = params if (!toolId) { throw new Error('Tool ID is required for editing a custom tool') } - // At least one of title, schema, or code must be provided - if (!title && !schema && !code) { - throw new Error('At least one of title, schema, or code must be provided for editing') + // At least one of schema or code must be provided + if (!schema && !code) { + throw new Error('At least one of schema or code must be provided for editing') } // We need to send the full tool data to the API for updates @@ -314,11 +310,12 @@ export class ManageCustomToolClientTool extends BaseClientTool { throw new Error(`Tool with ID ${toolId} not found`) } - // Merge updates with existing tool + // Merge updates with existing tool - use function name as title + const mergedSchema = schema ?? existingTool.schema const updatedTool = { id: toolId, - title: title ?? existingTool.title, - schema: schema ?? existingTool.schema, + title: mergedSchema.function.name, + schema: mergedSchema, code: code ?? existingTool.code, } @@ -337,14 +334,15 @@ export class ManageCustomToolClientTool extends BaseClientTool { throw new Error(data.error || 'Failed to update custom tool') } - logger.info(`Updated custom tool: ${updatedTool.title}`, { toolId }) + const functionName = updatedTool.schema.function.name + logger.info(`Updated custom tool: ${functionName}`, { toolId }) this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Updated custom tool "${updatedTool.title}"`, { + await this.markToolComplete(200, `Updated custom tool "${functionName}"`, { success: true, operation: 'edit', toolId, - title: updatedTool.title, + functionName, }) } diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts new file mode 100644 index 0000000000..3c4f68e680 --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts @@ -0,0 +1,340 @@ +import { Check, Loader2, Server, X, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' +import { createLogger } from '@/lib/logs/console/logger' +import { useCopilotStore } from '@/stores/panel/copilot/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface McpServerConfig { + name: string + transport: 'streamable-http' + url?: string + headers?: Record + timeout?: number + enabled?: boolean +} + +interface ManageMcpToolArgs { + operation: 'add' | 'edit' | 'delete' + serverId?: string + config?: McpServerConfig +} + +const API_ENDPOINT = '/api/mcp/servers' + +/** + * Client tool for creating, editing, and deleting MCP tool servers via the copilot. + */ +export class ManageMcpToolClientTool extends BaseClientTool { + static readonly id = 'manage_mcp_tool' + private currentArgs?: ManageMcpToolArgs + + constructor(toolCallId: string) { + super(toolCallId, ManageMcpToolClientTool.id, ManageMcpToolClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Managing MCP tool', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Manage MCP tool?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Managing MCP tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed MCP tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to manage MCP tool', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted managing MCP tool', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped managing MCP tool', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Allow', icon: Check }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined + + if (!operation) return undefined + + const serverName = params?.config?.name || params?.serverName + + const getActionText = (verb: 'present' | 'past' | 'gerund') => { + switch (operation) { + case 'add': + return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' + case 'edit': + return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' + case 'delete': + return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + } + } + + const shouldShowServerName = (currentState: ClientToolCallState) => { + if (operation === 'add') { + return currentState === ClientToolCallState.success + } + return true + } + + const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' + + switch (state) { + case ClientToolCallState.success: + return `${getActionText('past')}${nameText}` + case ClientToolCallState.executing: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.generating: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.pending: + return `${getActionText('present')}${nameText}?` + case ClientToolCallState.error: + return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` + case ClientToolCallState.aborted: + return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` + case ClientToolCallState.rejected: + return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` + } + return undefined + }, + } + + /** + * Gets the tool call args from the copilot store (needed before execute() is called) + */ + private getArgsFromStore(): ManageMcpToolArgs | undefined { + try { + const { toolCallsById } = useCopilotStore.getState() + const toolCall = toolCallsById[this.toolCallId] + return (toolCall as any)?.params as ManageMcpToolArgs | undefined + } catch { + return undefined + } + } + + /** + * Override getInterruptDisplays to only show confirmation for edit and delete operations. + * Add operations execute directly without confirmation. + */ + getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { + const args = this.currentArgs || this.getArgsFromStore() + const operation = args?.operation + if (operation === 'edit' || operation === 'delete') { + return this.metadata.interrupt + } + return undefined + } + + async handleReject(): Promise { + await super.handleReject() + this.setState(ClientToolCallState.rejected) + } + + async handleAccept(args?: ManageMcpToolArgs): Promise { + const logger = createLogger('ManageMcpToolClientTool') + try { + this.setState(ClientToolCallState.executing) + await this.executeOperation(args, logger) + } catch (e: any) { + logger.error('execute failed', { message: e?.message }) + this.setState(ClientToolCallState.error) + await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool') + } + } + + async execute(args?: ManageMcpToolArgs): Promise { + this.currentArgs = args + if (args?.operation === 'add') { + await this.handleAccept(args) + } + } + + /** + * Executes the MCP tool operation (add, edit, or delete) + */ + private async executeOperation( + args: ManageMcpToolArgs | undefined, + logger: ReturnType + ): Promise { + if (!args?.operation) { + throw new Error('Operation is required') + } + + const { operation, serverId, config } = args + + const { hydration } = useWorkflowRegistry.getState() + const workspaceId = hydration.workspaceId + if (!workspaceId) { + throw new Error('No active workspace found') + } + + logger.info(`Executing MCP tool operation: ${operation}`, { + operation, + serverId, + serverName: config?.name, + workspaceId, + }) + + switch (operation) { + case 'add': + await this.addMcpServer({ config, workspaceId }, logger) + break + case 'edit': + await this.editMcpServer({ serverId, config, workspaceId }, logger) + break + case 'delete': + await this.deleteMcpServer({ serverId, workspaceId }, logger) + break + default: + throw new Error(`Unknown operation: ${operation}`) + } + } + + /** + * Creates a new MCP server + */ + private async addMcpServer( + params: { + config?: McpServerConfig + workspaceId: string + }, + logger: ReturnType + ): Promise { + const { config, workspaceId } = params + + if (!config) { + throw new Error('Config is required for adding an MCP tool') + } + if (!config.name) { + throw new Error('Server name is required') + } + if (!config.url) { + throw new Error('Server URL is required for streamable-http transport') + } + + const serverData = { + ...config, + workspaceId, + transport: config.transport || 'streamable-http', + timeout: config.timeout || 30000, + enabled: config.enabled !== false, + } + + const response = await fetch(API_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(serverData), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to create MCP tool') + } + + const serverId = data.data?.serverId + logger.info(`Created MCP tool: ${config.name}`, { serverId }) + + this.setState(ClientToolCallState.success) + await this.markToolComplete(200, `Created MCP tool "${config.name}"`, { + success: true, + operation: 'add', + serverId, + serverName: config.name, + }) + } + + /** + * Updates an existing MCP server + */ + private async editMcpServer( + params: { + serverId?: string + config?: McpServerConfig + workspaceId: string + }, + logger: ReturnType + ): Promise { + const { serverId, config, workspaceId } = params + + if (!serverId) { + throw new Error('Server ID is required for editing an MCP tool') + } + + if (!config) { + throw new Error('Config is required for editing an MCP tool') + } + + const updateData = { + ...config, + workspaceId, + } + + const response = await fetch(`${API_ENDPOINT}/${serverId}?workspaceId=${workspaceId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to update MCP tool') + } + + const serverName = config.name || data.data?.server?.name || serverId + logger.info(`Updated MCP tool: ${serverName}`, { serverId }) + + this.setState(ClientToolCallState.success) + await this.markToolComplete(200, `Updated MCP tool "${serverName}"`, { + success: true, + operation: 'edit', + serverId, + serverName, + }) + } + + /** + * Deletes an MCP server + */ + private async deleteMcpServer( + params: { + serverId?: string + workspaceId: string + }, + logger: ReturnType + ): Promise { + const { serverId, workspaceId } = params + + if (!serverId) { + throw new Error('Server ID is required for deleting an MCP tool') + } + + const url = `${API_ENDPOINT}?serverId=${serverId}&workspaceId=${workspaceId}` + const response = await fetch(url, { + method: 'DELETE', + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to delete MCP tool') + } + + logger.info(`Deleted MCP tool: ${serverId}`) + + this.setState(ClientToolCallState.success) + await this.markToolComplete(200, `Deleted MCP tool`, { + success: true, + operation: 'delete', + serverId, + }) + } +} diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index ae4b2fc373..450844ea49 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -44,6 +44,7 @@ import { GetWorkflowDataClientTool } from '@/lib/copilot/tools/client/workflow/g import { GetWorkflowFromNameClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-from-name' import { ListUserWorkflowsClientTool } from '@/lib/copilot/tools/client/workflow/list-user-workflows' import { ManageCustomToolClientTool } from '@/lib/copilot/tools/client/workflow/manage-custom-tool' +import { ManageMcpToolClientTool } from '@/lib/copilot/tools/client/workflow/manage-mcp-tool' import { RunWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/run-workflow' import { SetGlobalWorkflowVariablesClientTool } from '@/lib/copilot/tools/client/workflow/set-global-workflow-variables' import { createLogger } from '@/lib/logs/console/logger' @@ -102,6 +103,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record any> = { check_deployment_status: (id) => new CheckDeploymentStatusClientTool(id), navigate_ui: (id) => new NavigateUIClientTool(id), manage_custom_tool: (id) => new ManageCustomToolClientTool(id), + manage_mcp_tool: (id) => new ManageMcpToolClientTool(id), } // Read-only static metadata for class-based tools (no instances) @@ -138,6 +140,7 @@ export const CLASS_TOOL_METADATA: Record CopilotStore) { try { @@ -882,6 +899,12 @@ const sseHandlers: Record = { const ctx = createExecutionContext({ toolCallId: id, toolName: name || 'unknown_tool' }) // Defer executing transition by a tick to let pending render setTimeout(() => { + // Guard against duplicate execution - check if already executing or terminal + const currentState = get().toolCallsById[id]?.state + if (currentState === ClientToolCallState.executing || isTerminalState(currentState)) { + return + } + const executingMap = { ...get().toolCallsById } executingMap[id] = { ...executingMap[id], @@ -984,6 +1007,12 @@ const sseHandlers: Record = { const hasInterrupt = !!inst?.getInterruptDisplays?.() if (!hasInterrupt && typeof inst?.execute === 'function') { setTimeout(() => { + // Guard against duplicate execution - check if already executing or terminal + const currentState = get().toolCallsById[id]?.state + if (currentState === ClientToolCallState.executing || isTerminalState(currentState)) { + return + } + const executingMap = { ...get().toolCallsById } executingMap[id] = { ...executingMap[id], diff --git a/bun.lock b/bun.lock index 453cf1a452..c9775d823c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", From 3cec449402846d300b8d20e9c6c8296d84553264 Mon Sep 17 00:00:00 2001 From: mosa Date: Wed, 10 Dec 2025 12:58:33 +0900 Subject: [PATCH 08/14] fix(ime): prevent form submission during IME composition steps (#2279) * fix(ui): prevent form submission during IME composition steps * chore(gitignore): add IntelliJ IDE files to .gitignore --------- Co-authored-by: Vikhyath Mondreti Co-authored-by: Waleed Co-authored-by: waleedlatif1 --- .gitignore | 3 +++ .../components/copilot/components/user-input/user-input.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d5fa99e481..6617532dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ start-collector.sh # VSCode .vscode +# IntelliJ +.idea + ## Helm Chart Tests helm/sim/test i18n.cache diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 801882ef2f..e222fec62e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -391,7 +391,7 @@ const UserInput = forwardRef( if (mentionKeyboard.handleArrowLeft(e)) return // Enter key handling - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault() if (!mentionMenu.showMentionMenu) { handleSubmit() From 0083c89fa5bfa0d8ee417cd810ea4b5b2b59b63f Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:50:28 -0800 Subject: [PATCH 09/14] feat(ui): logs, kb, emcn (#2207) * feat(kb): emcn alignment; sidebar: popover primary; settings-modal: expand * feat: EMCN breadcrumb; improvement(KB): UI * fix: hydration error * improvement(KB): UI * feat: emcn modal sizing, KB tags; refactor: deleted old sidebar * feat(logs): UI * fix: add documents modal name * feat: logs, emcn, cursorrules; refactor: logs * feat: dashboard * feat: notifications; improvement: logs details * fixed random rectangle on canvas * fixed the name of the file to align * fix build --------- Co-authored-by: waleed --- .cursor/rules/emcn-components.mdc | 45 + .cursor/rules/global.mdc | 20 + .cursor/rules/sim-architecture.mdc | 67 + .cursor/rules/sim-components.mdc | 64 + .cursor/rules/sim-hooks.mdc | 68 + .cursor/rules/sim-imports.mdc | 37 + .cursor/rules/sim-stores.mdc | 57 + .cursor/rules/sim-styling.mdc | 47 + .cursor/rules/sim-typescript.mdc | 24 + .cursorrules | 19 - CLAUDE.md | 47 + apps/sim/.cursorrules | 777 -------- .../landing-pricing/landing-pricing.tsx | 2 +- apps/sim/app/_styles/globals.css | 6 + apps/sim/app/api/logs/route.ts | 49 +- .../[id]/metrics/executions/route.ts | 60 +- apps/sim/app/invite/[id]/invite.tsx | 32 +- apps/sim/app/invite/[id]/utils.ts | 30 - apps/sim/app/templates/[id]/template.tsx | 73 +- .../templates/components/template-card.tsx | 2 +- apps/sim/app/templates/templates.tsx | 12 +- .../create-chunk-modal/create-chunk-modal.tsx | 54 +- .../delete-chunk-modal/delete-chunk-modal.tsx | 16 +- .../components/document-loading.tsx | 80 - .../document-tags-modal.tsx | 649 +++++++ .../edit-chunk-modal/edit-chunk-modal.tsx | 86 +- .../[id]/[documentId]/components/index.ts | 2 +- .../knowledge/[id]/[documentId]/document.tsx | 1050 +++++++---- .../[workspaceId]/knowledge/[id]/base.tsx | 1285 ++++++++------ .../[id]/components/action-bar/action-bar.tsx | 30 +- .../add-documents-modal.tsx | 366 ++++ .../base-tags-modal/base-tags-modal.tsx | 485 +++++ .../knowledge/[id]/components/index.ts | 4 +- .../knowledge-base-loading.tsx | 72 - .../components/upload-modal/upload-modal.tsx | 315 ---- .../components/base-card/base-card.tsx | 160 ++ .../base-overview/base-overview.tsx | 144 -- .../components/{shared.ts => constants.ts} | 0 .../create-base-modal/create-base-modal.tsx | 536 ++++++ .../components/create-modal/create-modal.tsx | 650 ------- .../document-tag-entry/document-tag-entry.tsx | 480 ----- .../empty-state-card/empty-state-card.tsx | 39 - .../knowledge/components/index.ts | 11 +- .../knowledge-header/knowledge-header.tsx | 181 +- .../primary-button/primary-button.tsx | 36 - .../components/search-input/search-input.tsx | 52 - .../knowledge-base-card-skeleton.tsx | 40 - .../components/skeletons/table-skeleton.tsx | 242 --- .../components/tag-input/tag-input.tsx | 197 --- .../workspace-selector/workspace-selector.tsx | 183 -- .../knowledge/hooks/use-knowledge-upload.ts | 94 +- .../[workspaceId]/knowledge/knowledge.tsx | 278 +-- .../[workspaceId]/knowledge/layout.tsx | 6 + .../[workspaceId]/knowledge/loading.tsx | 72 - .../[workspaceId]/knowledge/page.tsx | 6 +- .../[workspaceId]/knowledge/utils/sort.ts | 2 +- .../app/workspace/[workspaceId]/layout.tsx | 6 +- .../components/dashboard/components/index.ts | 3 + .../dashboard/components/line-chart/index.ts | 2 + .../line-chart}/line-chart.tsx | 272 ++- .../dashboard/components/status-bar/index.ts | 2 + .../status-bar}/status-bar.tsx | 25 +- .../components/workflows-list/index.ts | 2 + .../workflows-list/workflows-list.tsx | 115 ++ .../logs/components/dashboard/dashboard.tsx | 887 ++++++++++ .../logs/components/dashboard/index.ts | 1 + .../logs/components/dashboard/kpis.tsx | 38 - .../logs/components/dashboard/utils.ts | 17 - .../components/dashboard/workflow-details.tsx | 624 ------- .../components/dashboard/workflows-list.tsx | 137 -- .../filters/components/filter-section.tsx | 20 - .../components/filters/components/folder.tsx | 157 -- .../components/filters/components/index.ts | 6 - .../components/filters/components/level.tsx | 74 - .../components/filters/components/shared.ts | 31 - .../filters/components/timeline.tsx | 80 - .../components/filters/components/trigger.tsx | 113 -- .../filters/components/workflow.tsx | 155 -- .../logs/components/filters/filters.tsx | 77 - .../frozen-canvas/frozen-canvas-modal.tsx | 105 -- .../[workspaceId]/logs/components/index.ts | 11 + .../file-download/file-download.tsx | 264 +++ .../components/file-download/index.ts | 1 + .../frozen-canvas/frozen-canvas.tsx | 300 ++-- .../components/frozen-canvas/index.ts | 1 + .../components/trace-spans/index.ts | 1 + .../components/trace-spans/trace-spans.tsx | 630 +++++++ .../logs/components/log-details/index.ts | 1 + .../components/log-details/log-details.tsx | 336 ++++ .../components/controls}/controls.tsx | 82 +- .../logs-toolbar/components/controls/index.ts | 1 + .../slack-channel-selector/index.ts | 1 + .../slack-channel-selector.tsx | 12 +- .../components/workflow-selector/index.ts | 1 + .../workflow-selector}/workflow-selector.tsx | 108 +- .../components/notifications/index.ts | 1 + .../notifications/notifications.tsx | 1402 +++++++++++++++ .../logs-toolbar/components/search/index.ts | 1 + .../components}/search/search.tsx | 27 +- .../logs/components/logs-toolbar/index.ts | 4 + .../components/logs-toolbar/logs-toolbar.tsx | 483 +++++ .../components/notification-settings/index.ts | 2 - .../notification-settings.tsx | 1320 -------------- .../sidebar/components/file-download.tsx | 111 -- .../sidebar/components/markdown-renderer.tsx | 201 --- .../logs/components/sidebar/sidebar.tsx | 676 ------- .../tool-calls/tool-calls-display.tsx | 185 -- .../components/block-data-display.tsx | 71 - .../components/collapsible-input-output.tsx | 71 - .../components/trace-span-item.tsx | 734 -------- .../logs/components/trace-spans/index.ts | 4 - .../components/trace-spans/trace-spans.tsx | 278 --- .../logs/components/trace-spans/utils.ts | 133 -- .../[workspaceId]/logs/dashboard.tsx | 969 ---------- .../logs/hooks/use-search-state.ts | 2 +- .../workspace/[workspaceId]/logs/layout.tsx | 6 + .../app/workspace/[workspaceId]/logs/logs.tsx | 622 +++---- .../logs/{types/search.ts => types.ts} | 0 .../app/workspace/[workspaceId]/logs/utils.ts | 307 +++- .../templates/components/template-card.tsx | 12 +- .../[workspaceId]/templates/layout.tsx | 5 +- .../[workspaceId]/templates/templates.tsx | 54 +- .../w/[workflowId]/components/error/index.tsx | 8 +- .../components/mention-menu/mention-menu.tsx | 14 +- .../copilot/components/user-input/utils.ts | 18 - .../deploy-modal/components/chat/chat.tsx | 15 +- .../components/general/general.tsx | 9 +- .../components/template/template.tsx | 10 +- .../components/deploy-modal/deploy-modal.tsx | 8 +- .../channel-selector-input.tsx | 2 +- .../sub-block/components/code/code.tsx | 2 +- .../components/combobox/combobox.tsx | 2 +- .../condition-input/condition-input.tsx | 6 +- .../components/oauth-required-modal.tsx | 2 +- .../components/eval-input/eval-input.tsx | 5 +- .../file-selector/file-selector-input.tsx | 2 +- .../input-mapping/input-mapping.tsx | 3 +- .../knowledge-base-selector.tsx | 2 +- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 6 +- .../project-selector-input.tsx | 2 +- .../schedule-save/schedule-save.tsx | 2 +- .../components/short-input/short-input.tsx | 2 +- .../components/starter/input-format.tsx | 9 +- .../sub-block/components/table/table.tsx | 2 +- .../components/code-editor/code-editor.tsx | 2 +- .../custom-tool-modal/custom-tool-modal.tsx | 21 +- .../components/tool-input/tool-input.tsx | 27 +- .../components/trigger-save/trigger-save.tsx | 2 +- .../variables-input/variables-input.tsx | 2 +- .../panel/components/editor/editor.tsx | 11 - .../file-selector/file-selector-input.tsx | 2 +- .../w/[workflowId]/components/panel/panel.tsx | 2 +- .../skeleton-loading/skeleton-loading.tsx | 2 +- .../components/variables/variables.tsx | 4 +- .../workflow-block/workflow-block.tsx | 3 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 6 +- .../footer-navigation/footer-navigation.tsx | 150 -- .../components-new/footer-navigation/index.ts | 1 - .../sidebar/components-new/index.ts | 7 - .../workflow-list/workflow-list.tsx | 367 ---- .../workspace-header/workspace-header.tsx | 471 ----- .../components/create-menu/create-menu.tsx | 479 ----- .../floating-navigation.tsx | 65 - .../folder-tree/components/folder-item.tsx | 370 ---- .../folder-tree/components/workflow-item.tsx | 290 --- .../components/folder-tree/folder-tree.tsx | 587 ------- .../help-modal/help-modal.tsx | 50 +- .../w/components/sidebar/components/index.ts | 20 +- .../keyboard-shortcut/keyboard-shortcut.tsx | 32 - .../components/document-list.tsx | 59 - .../knowledge-base-tags.tsx | 527 ------ .../knowledge-tags/knowledge-tags.tsx | 796 --------- .../components/logs-filters/logs-filters.tsx | 37 - .../navigation-item/navigation-item.tsx | 68 - .../search-modal/search-modal.tsx | 2 +- .../search-modal/search-utils.ts | 0 .../components/api-keys/api-keys.tsx | 6 +- .../components/copilot/copilot.tsx | 5 +- .../components/custom-tools/custom-tools.tsx | 9 +- .../components/environment/environment.tsx | 6 +- .../settings-modal/components/files/files.tsx | 0 .../components/general/general.tsx | 10 +- .../settings-modal/components/index.ts | 0 .../components/integrations/integrations.tsx | 5 +- .../mcp/components/form-field/form-field.tsx | 0 .../formatted-input/formatted-input.tsx | 0 .../mcp/components/header-row/header-row.tsx | 0 .../components/mcp/components/index.ts | 0 .../mcp-server-skeleton.tsx | 0 .../server-list-item/server-list-item.tsx | 0 .../components/mcp/components/types.ts | 0 .../settings-modal/components/mcp/mcp.tsx | 5 +- .../components/shared/usage-header.tsx | 0 .../settings-modal/components/sso/sso.tsx | 0 .../cancel-subscription.tsx | 0 .../components/cancel-subscription/index.ts | 0 .../cost-breakdown/cost-breakdown.tsx | 0 .../components/cost-breakdown/index.ts | 0 .../credit-balance/credit-balance.tsx | 0 .../components/credit-balance/index.ts | 0 .../subscription/components/index.ts | 0 .../components/plan-card/index.ts | 0 .../components/plan-card/plan-card.tsx | 0 .../components/usage-limit/index.ts | 0 .../components/usage-limit/usage-limit.tsx | 0 .../components/subscription/plan-configs.ts | 2 +- .../subscription/subscription-permissions.ts | 0 .../components/subscription/subscription.tsx | 8 +- .../team-management/components/index.ts | 0 .../member-invitation-card.tsx | 0 .../no-organization-view.tsx | 0 .../remove-member-dialog.tsx | 0 .../components/team-members/team-members.tsx | 0 .../team-seats-overview.tsx | 0 .../components/team-seats/team-seats.tsx | 0 .../components/team-usage/team-usage.tsx | 4 +- .../team-management/team-management.tsx | 2 +- .../template-profile/template-profile.tsx | 7 +- .../hooks/use-profile-picture-upload.ts | 0 .../settings-modal/settings-modal.tsx | 4 +- .../subscription-modal/subscription-modal.tsx | 256 --- .../usage-indicator/usage-indicator.tsx | 0 .../workflow-context-menu.tsx | 57 - .../components/context-menu/context-menu.tsx | 40 +- .../components/delete-modal/delete-modal.tsx | 9 +- .../components/folder-item/folder-item.tsx | 4 +- .../workflow-list/components/index.ts | 0 .../workflow-item/avatars/avatars.tsx | 0 .../workflow-item/workflow-item.tsx | 15 +- .../workflow-list/workflow-list.tsx | 392 ++++- .../invite-modal/components/email-tag.tsx | 0 .../invite-modal/components/index.ts | 0 .../components/permission-selector.tsx | 0 .../components/permissions-table-skeleton.tsx | 0 .../components/permissions-table.tsx | 0 .../invite-modal/components/types.ts | 0 .../components/invite-modal/invite-modal.tsx | 6 +- .../workspace-header/index.ts | 0 .../workspace-header/workspace-header.tsx | 590 +++++-- .../workspace-selector/workspace-selector.tsx | 791 --------- .../w/components/sidebar/sidebar-new.tsx | 647 ------- .../w/components/sidebar/sidebar.tsx | 1559 +++++++---------- .../workflow-preview/workflow-preview.tsx | 16 +- .../app/workspace/[workspaceId]/w/page.tsx | 17 +- apps/sim/app/workspace/page.tsx | 2 +- apps/sim/blocks/index.ts | 11 +- apps/sim/blocks/registry.ts | 17 +- .../components/emcn/components/.cursorrules | 40 - .../emcn/components/breadcrumb/breadcrumb.tsx | 58 + .../emcn/components/combobox/combobox.tsx | 51 +- apps/sim/components/emcn/components/index.ts | 8 + .../emcn/components/modal/modal.tsx | 52 +- .../emcn/components/s-modal/s-modal.tsx | 2 +- .../emcn/icons/document-attachment.tsx | 35 + apps/sim/components/emcn/icons/eye.tsx | 27 + apps/sim/components/emcn/icons/index.ts | 3 + apps/sim/components/emcn/icons/library.tsx | 27 + apps/sim/hooks/queries/logs.ts | 42 +- apps/sim/hooks/queries/notifications.ts | 56 - apps/sim/lib/blog/code.tsx | 2 +- apps/sim/lib/knowledge/tags/service.ts | 3 +- apps/sim/lib/knowledge/tags/types.ts | 10 + apps/sim/lib/logs/get-trigger-options.ts | 11 +- apps/sim/lib/logs/search-suggestions.ts | 2 +- apps/sim/stores/logs/filters/store.ts | 12 +- apps/sim/stores/logs/filters/types.ts | 2 +- 266 files changed, 12062 insertions(+), 19297 deletions(-) create mode 100644 .cursor/rules/emcn-components.mdc create mode 100644 .cursor/rules/global.mdc create mode 100644 .cursor/rules/sim-architecture.mdc create mode 100644 .cursor/rules/sim-components.mdc create mode 100644 .cursor/rules/sim-hooks.mdc create mode 100644 .cursor/rules/sim-imports.mdc create mode 100644 .cursor/rules/sim-stores.mdc create mode 100644 .cursor/rules/sim-styling.mdc create mode 100644 .cursor/rules/sim-typescript.mdc delete mode 100644 .cursorrules create mode 100644 CLAUDE.md delete mode 100644 apps/sim/.cursorrules delete mode 100644 apps/sim/app/invite/[id]/utils.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx rename apps/sim/app/workspace/[workspaceId]/knowledge/components/{shared.ts => constants.ts} (100%) create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/empty-state-card/empty-state-card.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/primary-button/primary-button.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/search-input/search-input.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/knowledge-base-card-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/skeletons/table-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/workspace-selector/workspace-selector.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/layout.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/{ => components/line-chart}/line-chart.tsx (69%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/{ => components/status-bar}/status-bar.tsx (74%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/kpis.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/utils.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/workflow-details.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/workflows-list.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/shared.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/{ => log-details/components}/frozen-canvas/frozen-canvas.tsx (66%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx rename apps/sim/app/workspace/[workspaceId]/logs/components/{dashboard => logs-toolbar/components/controls}/controls.tsx (71%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/controls/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/{notification-settings => logs-toolbar/components/notifications/components/slack-channel-selector}/slack-channel-selector.tsx (87%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/{notification-settings => logs-toolbar/components/notifications/components/workflow-selector}/workflow-selector.tsx (51%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/search/index.ts rename apps/sim/app/workspace/[workspaceId]/logs/components/{ => logs-toolbar/components}/search/search.tsx (90%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/notification-settings/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/notification-settings/notification-settings.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/components/file-download.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/components/block-data-display.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/components/collapsible-input-output.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/components/trace-span-item.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/utils.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/dashboard.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/layout.tsx rename apps/sim/app/workspace/[workspaceId]/logs/{types/search.ts => types.ts} (100%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/footer-navigation/footer-navigation.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/footer-navigation/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/workflow-list.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workspace-header/workspace-header.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/create-menu/create-menu.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/floating-navigation/floating-navigation.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/workflow-item.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/folder-tree.tsx rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/help-modal/help-modal.tsx (91%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/keyboard-shortcut/keyboard-shortcut.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/knowledge-base-tags/components/document-list.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/knowledge-base-tags/knowledge-base-tags.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/knowledge-tags/knowledge-tags.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/logs-filters/logs-filters.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/navigation-item/navigation-item.tsx rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/search-modal/search-modal.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/search-modal/search-utils.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/api-keys/api-keys.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/copilot/copilot.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/custom-tools/custom-tools.tsx (98%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/environment/environment.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/files/files.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/general/general.tsx (98%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/integrations/integrations.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/form-field/form-field.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/formatted-input/formatted-input.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/header-row/header-row.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/components/types.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/mcp/mcp.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/shared/usage-header.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/sso/sso.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/cancel-subscription/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/cost-breakdown/cost-breakdown.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/cost-breakdown/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/credit-balance/credit-balance.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/credit-balance/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/plan-card/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/plan-card/plan-card.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/usage-limit/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/plan-configs.ts (92%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/subscription-permissions.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/subscription/subscription.tsx (98%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/remove-member-dialog/remove-member-dialog.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/team-members/team-members.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/team-seats/team-seats.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/components/team-usage/team-usage.tsx (97%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/team-management/team-management.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/components/template-profile/template-profile.tsx (98%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/hooks/use-profile-picture-upload.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/settings-modal/settings-modal.tsx (99%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/usage-indicator/usage-indicator.tsx (100%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-context-menu/workflow-context-menu.tsx rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/context-menu/context-menu.tsx (85%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/delete-modal/delete-modal.tsx (95%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/folder-item/folder-item.tsx (98%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/workflow-item/avatars/avatars.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workflow-list/components/workflow-item/workflow-item.tsx (95%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/email-tag.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/index.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/permission-selector.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/permissions-table-skeleton.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/permissions-table.tsx (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/components/types.ts (100%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/components/invite-modal/invite-modal.tsx (99%) rename apps/sim/app/workspace/[workspaceId]/w/components/sidebar/{components-new => components}/workspace-header/index.ts (100%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/workspace-selector.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar-new.tsx delete mode 100644 apps/sim/components/emcn/components/.cursorrules create mode 100644 apps/sim/components/emcn/components/breadcrumb/breadcrumb.tsx create mode 100644 apps/sim/components/emcn/icons/document-attachment.tsx create mode 100644 apps/sim/components/emcn/icons/eye.tsx create mode 100644 apps/sim/components/emcn/icons/library.tsx diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc new file mode 100644 index 0000000000..eac2bc7d19 --- /dev/null +++ b/.cursor/rules/emcn-components.mdc @@ -0,0 +1,45 @@ +--- +description: EMCN component library patterns with CVA +globs: ["apps/sim/components/emcn/**"] +--- + +# EMCN Component Guidelines + +## When to Use CVA vs Direct Styles + +**Use CVA (class-variance-authority) when:** +- 2+ visual variants (primary, secondary, outline) +- Multiple sizes or state variations +- Example: Button with variants + +**Use direct className when:** +- Single consistent style +- No variations needed +- Example: Label with one style + +## Patterns + +**With CVA:** +```tsx +const buttonVariants = cva('base-classes', { + variants: { + variant: { default: '...', primary: '...' }, + size: { sm: '...', md: '...' } + } +}) +export { Button, buttonVariants } +``` + +**Without CVA:** +```tsx +function Label({ className, ...props }) { + return +} +``` + +## Rules +- Use Radix UI primitives for accessibility +- Export component and variants (if using CVA) +- TSDoc with usage examples +- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]` +- Always use `transition-colors` for hover states diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc new file mode 100644 index 0000000000..d4559a1eb4 --- /dev/null +++ b/.cursor/rules/global.mdc @@ -0,0 +1,20 @@ +--- +description: Global coding standards that apply to all files +alwaysApply: true +--- + +# Global Standards + +You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient. + +## Logging +Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. + +## Comments +Use TSDoc for documentation. No `====` separators. No non-TSDoc comments. + +## Styling +Never update global styles. Keep all styling local to components. + +## Package Manager +Use `bun` and `bunx`, not `npm` and `npx`. diff --git a/.cursor/rules/sim-architecture.mdc b/.cursor/rules/sim-architecture.mdc new file mode 100644 index 0000000000..9cc5b83c6d --- /dev/null +++ b/.cursor/rules/sim-architecture.mdc @@ -0,0 +1,67 @@ +--- +description: Core architecture principles for the Sim app +globs: ["apps/sim/**"] +--- + +# Sim App Architecture + +## Core Principles +1. **Single Responsibility**: Each component, hook, store has one clear purpose +2. **Composition Over Complexity**: Break down complex logic into smaller pieces +3. **Type Safety First**: TypeScript interfaces for all props, state, return types +4. **Predictable State**: Zustand for global state, useState for UI-only concerns +5. **Performance by Default**: useMemo, useCallback, refs appropriately + +## File Organization + +``` +feature/ +├── components/ # Feature components +│ └── sub-feature/ # Sub-feature with own components +├── hooks/ # Custom hooks +└── feature.tsx # Main component +``` + +## Naming Conventions +- **Components**: PascalCase (`WorkflowList`, `TriggerPanel`) +- **Hooks**: camelCase with `use` prefix (`useWorkflowOperations`) +- **Files**: kebab-case matching export (`workflow-list.tsx`) +- **Stores**: kebab-case in stores/ (`sidebar/store.ts`) +- **Constants**: SCREAMING_SNAKE_CASE +- **Interfaces**: PascalCase with suffix (`WorkflowListProps`) + +## State Management + +**useState**: UI-only concerns (dropdown open, hover, form inputs) +**Zustand**: Shared state, persistence, global app state +**useRef**: DOM refs, avoiding dependency issues, mutable non-reactive values + +## Component Extraction + +**Extract to separate file when:** +- Complex (50+ lines) +- Used across 2+ files +- Has own state/logic + +**Keep inline when:** +- Simple (< 10 lines) +- Used in only 1 file +- Purely presentational + +**Never import utilities from another component file.** Extract shared helpers to `lib/` or `utils/`. + +## Utils Files + +**Never create a `utils.ts` file for a single consumer.** Inline the logic directly in the consuming component. + +**Create `utils.ts` when:** +- 2+ files import the same helper + +**Prefer existing sources of truth:** +- Before duplicating logic, check if a centralized helper already exists (e.g., `lib/logs/get-trigger-options.ts`) +- Import from the source of truth rather than creating wrapper functions + +**Location hierarchy:** +- `lib/` — App-wide utilities (auth, billing, core) +- `feature/utils.ts` — Feature-scoped utilities (used by 2+ components in the feature) +- Inline — Single-use helpers (define directly in the component) diff --git a/.cursor/rules/sim-components.mdc b/.cursor/rules/sim-components.mdc new file mode 100644 index 0000000000..d7eb4b8a18 --- /dev/null +++ b/.cursor/rules/sim-components.mdc @@ -0,0 +1,64 @@ +--- +description: Component patterns and structure for React components +globs: ["apps/sim/**/*.tsx"] +--- + +# Component Patterns + +## Structure Order +```typescript +'use client' // Only if using hooks + +// 1. Imports (external → internal → relative) +// 2. Constants at module level +const CONFIG = { SPACING: 8 } as const + +// 3. Props interface with TSDoc +interface ComponentProps { + /** Description */ + requiredProp: string + optionalProp?: boolean +} + +// 4. Component with TSDoc +export function Component({ requiredProp, optionalProp = false }: ComponentProps) { + // a. Refs + // b. External hooks (useParams, useRouter) + // c. Store hooks + // d. Custom hooks + // e. Local state + // f. useMemo computations + // g. useCallback handlers + // h. useEffect + // i. Return JSX +} +``` + +## Rules +1. Add `'use client'` when using React hooks +2. Always define props interface +3. TSDoc on component: description, @param, @returns +4. Extract constants with `as const` +5. Use Tailwind only, no inline styles +6. Semantic HTML (`aside`, `nav`, `article`) +7. Include ARIA attributes where appropriate +8. Optional chain callbacks: `onAction?.(id)` + +## Factory Pattern with Caching + +When generating components for a specific signature (e.g., icons): + +```typescript +const cache = new Map>() + +function getColorIcon(color: string) { + if (cache.has(color)) return cache.get(color)! + + const Icon = ({ className }: { className?: string }) => ( +
+ ) + Icon.displayName = `ColorIcon(${color})` + cache.set(color, Icon) + return Icon +} +``` diff --git a/.cursor/rules/sim-hooks.mdc b/.cursor/rules/sim-hooks.mdc new file mode 100644 index 0000000000..fce15c2165 --- /dev/null +++ b/.cursor/rules/sim-hooks.mdc @@ -0,0 +1,68 @@ +--- +description: Custom hook patterns and best practices +globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"] +--- + +# Hook Patterns + +## Structure +```typescript +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('useFeatureName') + +interface UseFeatureProps { + id: string + onSuccess?: (result: Result) => void +} + +/** + * Hook description. + * @param props - Configuration + * @returns State and operations + */ +export function useFeature({ id, onSuccess }: UseFeatureProps) { + // 1. Refs for stable dependencies + const idRef = useRef(id) + const onSuccessRef = useRef(onSuccess) + + // 2. State + const [data, setData] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + // 3. Sync refs + useEffect(() => { + idRef.current = id + onSuccessRef.current = onSuccess + }, [id, onSuccess]) + + // 4. Operations with useCallback + const fetchData = useCallback(async () => { + setIsLoading(true) + try { + const result = await fetch(`/api/${idRef.current}`).then(r => r.json()) + setData(result) + onSuccessRef.current?.(result) + } catch (err) { + setError(err as Error) + logger.error('Failed', { error: err }) + } finally { + setIsLoading(false) + } + }, []) // Empty deps - using refs + + // 5. Return grouped by state/operations + return { data, isLoading, error, fetchData } +} +``` + +## Rules +1. Single responsibility per hook +2. Props interface required +3. TSDoc required +4. Use logger, not console.log +5. Refs for stable callback dependencies +6. Wrap returned functions in useCallback +7. Always try/catch async operations +8. Track loading/error states diff --git a/.cursor/rules/sim-imports.mdc b/.cursor/rules/sim-imports.mdc new file mode 100644 index 0000000000..2eb45b0ef1 --- /dev/null +++ b/.cursor/rules/sim-imports.mdc @@ -0,0 +1,37 @@ +--- +description: Import patterns for the Sim application +globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"] +--- + +# Import Patterns + +## EMCN Components +Import from `@/components/emcn`, never from subpaths like `@/components/emcn/components/modal/modal`. + +**Exception**: CSS imports use actual file paths: `import '@/components/emcn/components/code/code.css'` + +## Feature Components +Import from central folder indexes, not specific subfolders: +```typescript +// ✅ Correct +import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components' + +// ❌ Wrong +import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard' +``` + +## Internal vs External +- **Cross-feature**: Absolute paths through central index +- **Within feature**: Relative paths (`./components/...`, `../utils`) + +## Import Order +1. React/core libraries +2. External libraries +3. UI components (`@/components/emcn`, `@/components/ui`) +4. Utilities (`@/lib/...`) +5. Feature imports from indexes +6. Relative imports +7. CSS imports + +## Types +Use `type` keyword: `import type { WorkflowLog } from '...'` diff --git a/.cursor/rules/sim-stores.mdc b/.cursor/rules/sim-stores.mdc new file mode 100644 index 0000000000..56f4ea365c --- /dev/null +++ b/.cursor/rules/sim-stores.mdc @@ -0,0 +1,57 @@ +--- +description: Zustand store patterns +globs: ["apps/sim/**/store.ts", "apps/sim/**/stores/**/*.ts"] +--- + +# Zustand Store Patterns + +## Structure +```typescript +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface FeatureState { + // State + items: Item[] + activeId: string | null + + // Actions + setItems: (items: Item[]) => void + addItem: (item: Item) => void + clearState: () => void +} + +const createInitialState = () => ({ + items: [], + activeId: null, +}) + +export const useFeatureStore = create()( + persist( + (set) => ({ + ...createInitialState(), + + setItems: (items) => set({ items }), + + addItem: (item) => set((state) => ({ + items: [...state.items, item], + })), + + clearState: () => set(createInitialState()), + }), + { + name: 'feature-state', + partialize: (state) => ({ items: state.items }), + } + ) +) +``` + +## Rules +1. Interface includes state and actions +2. Extract config to module constants +3. TSDoc on store +4. Only persist what's needed +5. Immutable updates only - never mutate +6. Use `set((state) => ...)` when depending on previous state +7. Provide clear/reset actions diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc new file mode 100644 index 0000000000..18dfc8af43 --- /dev/null +++ b/.cursor/rules/sim-styling.mdc @@ -0,0 +1,47 @@ +--- +description: Tailwind CSS and styling conventions +globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"] +--- + +# Styling Rules + +## Tailwind +1. **No inline styles** - Use Tailwind classes exclusively +2. **No duplicate dark classes** - Don't add `dark:` when value matches light mode +3. **Exact values** - Use design system values (`text-[14px]`, `h-[25px]`) +4. **Prefer px** - Use `px-[4px]` over `px-1` +5. **Transitions** - Add `transition-colors` for interactive states + +## Conditional Classes +```typescript +import { cn } from '@/lib/utils' + +
+``` + +## CSS Variables for Dynamic Styles +```typescript +// In store setter +setSidebarWidth: (width) => { + set({ sidebarWidth: width }) + document.documentElement.style.setProperty('--sidebar-width', `${width}px`) +} + +// In component +