diff --git a/apps/sim/app/api/a2a/serve/[agentId]/route.ts b/apps/sim/app/api/a2a/serve/[agentId]/route.ts index dfd1f4e8f2..aed8c3c205 100644 --- a/apps/sim/app/api/a2a/serve/[agentId]/route.ts +++ b/apps/sim/app/api/a2a/serve/[agentId]/route.ts @@ -384,7 +384,7 @@ async function handleMessageSend( headers, body: JSON.stringify({ ...workflowInput, - triggerType: 'api', + triggerType: 'a2a', ...(useInternalAuth && { workflowId: agent.workflowId }), }), signal: AbortSignal.timeout(A2A_DEFAULT_TIMEOUT), @@ -613,7 +613,7 @@ async function handleMessageStream( headers, body: JSON.stringify({ ...workflowInput, - triggerType: 'api', + triggerType: 'a2a', stream: true, ...(useInternalAuth && { workflowId: agent.workflowId }), }), diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index e045e6eaba..3a9b04dfba 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -27,7 +27,7 @@ import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata, IterationContext } from '@/executor/execution/types' import type { StreamingExecution } from '@/executor/types' import { Serializer } from '@/serializer' -import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' +import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types' const logger = createLogger('WorkflowExecuteAPI') @@ -109,7 +109,7 @@ type AsyncExecutionParams = { workflowId: string userId: string input: any - triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp' + triggerType: CoreTriggerType } /** @@ -253,17 +253,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: }) const executionId = uuidv4() - type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp' - let loggingTriggerType: LoggingTriggerType = 'manual' - if ( - triggerType === 'api' || - triggerType === 'chat' || - triggerType === 'webhook' || - triggerType === 'schedule' || - triggerType === 'manual' || - triggerType === 'mcp' - ) { - loggingTriggerType = triggerType as LoggingTriggerType + let loggingTriggerType: CoreTriggerType = 'manual' + if (CORE_TRIGGER_TYPES.includes(triggerType as CoreTriggerType)) { + loggingTriggerType = triggerType as CoreTriggerType } const loggingSession = new LoggingSession( workflowId, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index 5800c6614e..5c4d83c54e 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -72,6 +72,7 @@ const TRIGGER_VARIANT_MAP: Record['va schedule: 'green', chat: 'purple', webhook: 'orange', + a2a: 'teal', } interface StatusBadgeProps { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx index 38e59dcbcb..81fd828842 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx @@ -204,7 +204,8 @@ export function A2aDeploy({ const [skillTags, setSkillTags] = useState([]) const [language, setLanguage] = useState('curl') const [useStreamingExample, setUseStreamingExample] = useState(false) - const [copied, setCopied] = useState(false) + const [urlCopied, setUrlCopied] = useState(false) + const [codeCopied, setCodeCopied] = useState(false) useEffect(() => { if (existingAgent) { @@ -451,7 +452,7 @@ export function A2aDeploy({ } try { - if (!isDeployed && onDeployWorkflow) { + if ((!isDeployed || workflowNeedsRedeployment) && onDeployWorkflow) { await onDeployWorkflow() } @@ -475,6 +476,7 @@ export function A2aDeploy({ }, [ existingAgent, isDeployed, + workflowNeedsRedeployment, onDeployWorkflow, name, description, @@ -643,8 +645,8 @@ console.log(data);` const handleCopyCommand = useCallback(() => { navigator.clipboard.writeText(getCurlCommand()) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setCodeCopied(true) + setTimeout(() => setCodeCopied(false), 2000) }, [getCurlCommand]) if (isLoading) { @@ -702,12 +704,12 @@ console.log(data);` type='button' onClick={() => { navigator.clipboard.writeText(endpoint) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setUrlCopied(true) + setTimeout(() => setUrlCopied(false), 2000) }} className='-translate-y-1/2 absolute top-1/2 right-2' > - {copied ? ( + {urlCopied ? ( ) : ( @@ -715,7 +717,7 @@ console.log(data);` - {copied ? 'Copied' : 'Copy'} + {urlCopied ? 'Copied' : 'Copy'} @@ -871,11 +873,15 @@ console.log(data);` aria-label='Copy command' className='!p-1.5 -my-1.5' > - {copied ? : } + {codeCopied ? ( + + ) : ( + + )} - {copied ? 'Copied' : 'Copy'} + {codeCopied ? 'Copied' : 'Copy'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 2ac4f3cb72..f4e2b54883 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2319,6 +2319,8 @@ const WorkflowContent = React.memo(() => { /** * Handles connection drag end. Detects if the edge was dropped over a block * and automatically creates a connection to that block's target handle. + * Only creates a connection if ReactFlow didn't already handle it (e.g., when + * dropping on the block body instead of a handle). */ const onConnectEnd = useCallback( (event: MouseEvent | TouchEvent) => { @@ -2340,14 +2342,25 @@ const WorkflowContent = React.memo(() => { // Find node under cursor const targetNode = findNodeAtPosition(flowPosition) - // Create connection if valid target found + // Create connection if valid target found AND edge doesn't already exist + // ReactFlow's onConnect fires first when dropping on a handle, so we check + // if that connection already exists to avoid creating duplicates. + // IMPORTANT: We must read directly from the store (not React state) because + // the store update from ReactFlow's onConnect may not have triggered a + // React re-render yet when this callback runs (typically 1-2ms later). if (targetNode && targetNode.id !== source.nodeId) { - onConnect({ - source: source.nodeId, - sourceHandle: source.handleId, - target: targetNode.id, - targetHandle: 'target', - }) + const currentEdges = useWorkflowStore.getState().edges + const edgeAlreadyExists = currentEdges.some( + (e) => e.source === source.nodeId && e.target === targetNode.id + ) + if (!edgeAlreadyExists) { + onConnect({ + source: source.nodeId, + sourceHandle: source.handleId, + target: targetNode.id, + targetHandle: 'target', + }) + } } connectionSourceRef.current = null diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts index 9bb1686d66..bbe8a29e58 100644 --- a/apps/sim/background/workflow-execution.ts +++ b/apps/sim/background/workflow-execution.ts @@ -10,6 +10,7 @@ import { getWorkflowById } from '@/lib/workflows/utils' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata } from '@/executor/execution/types' import type { ExecutionResult } from '@/executor/types' +import type { CoreTriggerType } from '@/stores/logs/filters/types' const logger = createLogger('TriggerWorkflowExecution') @@ -17,7 +18,7 @@ export type WorkflowExecutionPayload = { workflowId: string userId: string input?: any - triggerType?: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp' + triggerType?: CoreTriggerType metadata?: Record } diff --git a/apps/sim/components/emcn/components/badge/badge.tsx b/apps/sim/components/emcn/components/badge/badge.tsx index 4e363178ed..7b728df058 100644 --- a/apps/sim/components/emcn/components/badge/badge.tsx +++ b/apps/sim/components/emcn/components/badge/badge.tsx @@ -25,6 +25,7 @@ const badgeVariants = cva( orange: `${STATUS_BASE} bg-[#fed7aa] text-[#c2410c] dark:bg-[rgba(249,115,22,0.2)] dark:text-[#fdba74]`, amber: `${STATUS_BASE} bg-[#fde68a] text-[#a16207] dark:bg-[rgba(245,158,11,0.2)] dark:text-[#fcd34d]`, teal: `${STATUS_BASE} bg-[#99f6e4] text-[#0f766e] dark:bg-[rgba(20,184,166,0.2)] dark:text-[#5eead4]`, + cyan: `${STATUS_BASE} bg-[#a5f3fc] text-[#0e7490] dark:bg-[rgba(14,165,233,0.2)] dark:text-[#7dd3fc]`, 'gray-secondary': `${STATUS_BASE} bg-[var(--surface-4)] text-[var(--text-secondary)]`, }, size: { @@ -51,6 +52,7 @@ const STATUS_VARIANTS = [ 'orange', 'amber', 'teal', + 'cyan', 'gray-secondary', ] as const @@ -84,7 +86,7 @@ export interface BadgeProps * Supports two categories of variants: * - **Bordered**: `default`, `outline` - traditional badges with borders * - **Status colors**: `green`, `red`, `gray`, `blue`, `blue-secondary`, `purple`, - * `orange`, `amber`, `teal`, `gray-secondary` - borderless colored badges + * `orange`, `amber`, `teal`, `cyan`, `gray-secondary` - borderless colored badges * * Status color variants can display a dot indicator via the `dot` prop. * All variants support an optional `icon` prop for leading icons. diff --git a/apps/sim/hooks/queries/notifications.ts b/apps/sim/hooks/queries/notifications.ts index ebe8b5b1f4..49af2ed8d5 100644 --- a/apps/sim/hooks/queries/notifications.ts +++ b/apps/sim/hooks/queries/notifications.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import type { CoreTriggerType } from '@/stores/logs/filters/types' const logger = createLogger('NotificationQueries') @@ -18,7 +19,7 @@ export const notificationKeys = { type NotificationType = 'webhook' | 'email' | 'slack' type LogLevel = 'info' | 'error' -type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp' +type TriggerType = CoreTriggerType type AlertRuleType = | 'consecutive_failures' diff --git a/apps/sim/lib/a2a/utils.ts b/apps/sim/lib/a2a/utils.ts index 84c34c77fe..119059d994 100644 --- a/apps/sim/lib/a2a/utils.ts +++ b/apps/sim/lib/a2a/utils.ts @@ -78,12 +78,16 @@ export interface A2AFile { export function extractFileContent(message: Message): A2AFile[] { return message.parts .filter((part): part is FilePart => part.kind === 'file') - .map((part) => ({ - name: part.file.name, - mimeType: part.file.mimeType, - ...('uri' in part.file ? { uri: part.file.uri } : {}), - ...('bytes' in part.file ? { bytes: part.file.bytes } : {}), - })) + .map((part) => { + const file = part.file as unknown as Record + const uri = (file.url as string) || (file.uri as string) + return { + name: file.name as string | undefined, + mimeType: file.mimeType as string | undefined, + ...(uri ? { uri } : {}), + ...(file.bytes ? { bytes: file.bytes as string } : {}), + } + }) } export interface ExecutionFileInput { diff --git a/apps/sim/lib/core/rate-limiter/types.ts b/apps/sim/lib/core/rate-limiter/types.ts index 0834461f9a..282ee09e03 100644 --- a/apps/sim/lib/core/rate-limiter/types.ts +++ b/apps/sim/lib/core/rate-limiter/types.ts @@ -1,15 +1,8 @@ import { env } from '@/lib/core/config/env' +import type { CoreTriggerType } from '@/stores/logs/filters/types' import type { TokenBucketConfig } from './storage' -export type TriggerType = - | 'api' - | 'webhook' - | 'schedule' - | 'manual' - | 'chat' - | 'mcp' - | 'form' - | 'api-endpoint' +export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint' export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint' diff --git a/apps/sim/lib/execution/preprocessing.ts b/apps/sim/lib/execution/preprocessing.ts index b1487828c8..8ff7afd8d9 100644 --- a/apps/sim/lib/execution/preprocessing.ts +++ b/apps/sim/lib/execution/preprocessing.ts @@ -7,6 +7,7 @@ import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' +import type { CoreTriggerType } from '@/stores/logs/filters/types' const logger = createLogger('ExecutionPreprocessing') @@ -108,7 +109,7 @@ export interface PreprocessExecutionOptions { // Required fields workflowId: string userId: string // The authenticated user ID - triggerType: 'manual' | 'api' | 'webhook' | 'schedule' | 'chat' | 'mcp' | 'form' + triggerType: CoreTriggerType | 'form' executionId: string requestId: string diff --git a/apps/sim/lib/logs/get-trigger-options.ts b/apps/sim/lib/logs/get-trigger-options.ts index 0f05a077d8..fd704c7755 100644 --- a/apps/sim/lib/logs/get-trigger-options.ts +++ b/apps/sim/lib/logs/get-trigger-options.ts @@ -38,6 +38,7 @@ export function getTriggerOptions(): TriggerOption[] { { value: 'form', label: 'Form', color: '#06b6d4' }, { value: 'webhook', label: 'Webhook', color: '#ea580c' }, { value: 'mcp', label: 'MCP', color: '#dc2626' }, + { value: 'a2a', label: 'A2A', color: '#14b8a6' }, ] for (const trigger of triggers) { diff --git a/apps/sim/stores/logs/filters/types.ts b/apps/sim/stores/logs/filters/types.ts index c44ee3bafb..dde0bb9303 100644 --- a/apps/sim/stores/logs/filters/types.ts +++ b/apps/sim/stores/logs/filters/types.ts @@ -174,7 +174,15 @@ export type TimeRange = export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {}) /** Core trigger types for workflow execution */ -export const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const +export const CORE_TRIGGER_TYPES = [ + 'manual', + 'api', + 'schedule', + 'chat', + 'webhook', + 'mcp', + 'a2a', +] as const export type CoreTriggerType = (typeof CORE_TRIGGER_TYPES)[number]