From 40a7825e360f138795c94c82e2e4a506358329cc Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 14:42:23 -0800 Subject: [PATCH 1/6] Fix copilot diff controls --- .../components/diff-controls/diff-controls.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx index f87ce0130c..9ce63f5e9b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx @@ -8,6 +8,7 @@ import { useNotificationStore } from '@/stores/notifications' import { useCopilotStore, usePanelStore } from '@/stores/panel' import { useTerminalStore } from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('DiffControls') const NOTIFICATION_WIDTH = 240 @@ -37,8 +38,15 @@ export const DiffControls = memo(function DiffControls() { ) ) + const { activeWorkflowId } = useWorkflowRegistry( + useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), []) + ) + const allNotifications = useNotificationStore((state) => state.notifications) - const hasVisibleNotifications = allNotifications.length > 0 + const hasVisibleNotifications = useMemo(() => { + if (!activeWorkflowId) return false + return allNotifications.some((n) => !n.workflowId || n.workflowId === activeWorkflowId) + }, [allNotifications, activeWorkflowId]) const handleAccept = useCallback(() => { logger.info('Accepting proposed changes with backup protection') From af06c4f606e48b6390c844081a19970dda5090e6 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 14:59:57 -0800 Subject: [PATCH 2/6] Fix router block for copilot --- .../tools/server/workflow/edit-workflow.ts | 98 +++++++++++++ .../workflows/sanitization/json-sanitizer.ts | 129 +++++++++++++++++- 2 files changed, 223 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 794a4bb885..a17af3cd3a 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -878,6 +878,25 @@ function validateSourceHandleForBlock( error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, ${EDGE.ROUTER_PREFIX}{targetId}, error`, } + case 'router_v2': { + if (!sourceHandle.startsWith(EDGE.ROUTER_PREFIX)) { + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for router_v2 block. Must start with "${EDGE.ROUTER_PREFIX}"`, + } + } + + const routesValue = sourceBlock?.subBlocks?.routes?.value + if (!routesValue) { + return { + valid: false, + error: `Invalid router handle "${sourceHandle}" - no routes defined`, + } + } + + return validateRouterHandle(sourceHandle, sourceBlock.id, routesValue) + } + default: if (sourceHandle === 'source') { return { valid: true } @@ -963,6 +982,85 @@ function validateConditionHandle( } } +/** + * Validates router handle references a valid route in the block. + * Accepts both internal IDs (router-{routeId}) and semantic keys (router-{blockId}-route-1) + */ +function validateRouterHandle( + sourceHandle: string, + blockId: string, + routesValue: string | any[] +): EdgeHandleValidationResult { + let routes: any[] + if (typeof routesValue === 'string') { + try { + routes = JSON.parse(routesValue) + } catch { + return { + valid: false, + error: `Cannot validate router handle "${sourceHandle}" - routes is not valid JSON`, + } + } + } else if (Array.isArray(routesValue)) { + routes = routesValue + } else { + return { + valid: false, + error: `Cannot validate router handle "${sourceHandle}" - routes is not an array`, + } + } + + if (!Array.isArray(routes) || routes.length === 0) { + return { + valid: false, + error: `Invalid router handle "${sourceHandle}" - no routes defined`, + } + } + + const validHandles = new Set() + const semanticPrefix = `router-${blockId}-` + + for (let i = 0; i < routes.length; i++) { + const route = routes[i] + + // Accept internal ID format: router-{uuid} + if (route.id) { + validHandles.add(`router-${route.id}`) + } + + // Accept 1-indexed route number format: router-{blockId}-route-1, router-{blockId}-route-2, etc. + validHandles.add(`${semanticPrefix}route-${i + 1}`) + + // Accept normalized title format: router-{blockId}-{normalized-title} + // Normalize: lowercase, replace spaces with dashes, remove special chars + if (route.title && typeof route.title === 'string') { + const normalizedTitle = route.title + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') + if (normalizedTitle) { + validHandles.add(`${semanticPrefix}${normalizedTitle}`) + } + } + } + + if (validHandles.has(sourceHandle)) { + return { valid: true } + } + + const validOptions = Array.from(validHandles).slice(0, 5) + const moreCount = validHandles.size - validOptions.length + let validOptionsStr = validOptions.join(', ') + if (moreCount > 0) { + validOptionsStr += `, ... and ${moreCount} more` + } + + return { + valid: false, + error: `Invalid router handle "${sourceHandle}". Valid handles: ${validOptionsStr}`, + } +} + /** * Validates target handle is valid (must be 'target') */ diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index 90cd990766..b62974f4be 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -268,12 +268,130 @@ function sanitizeSubBlocks( return sanitized } +/** + * Convert internal condition handle (condition-{uuid}) to semantic format (condition-{blockId}-if) + */ +function convertConditionHandleToSemantic( + handle: string, + blockId: string, + block: BlockState +): string { + if (!handle.startsWith('condition-')) { + return handle + } + + // Extract the condition UUID from the handle + const conditionId = handle.substring('condition-'.length) + + // Get conditions from block subBlocks + const conditionsValue = block.subBlocks?.conditions?.value + if (!conditionsValue || typeof conditionsValue !== 'string') { + return handle + } + + let conditions: Array<{ id: string; title: string }> + try { + conditions = JSON.parse(conditionsValue) + } catch { + return handle + } + + if (!Array.isArray(conditions)) { + return handle + } + + // Find the condition by ID and generate semantic handle + let elseIfCount = 0 + for (const condition of conditions) { + const title = condition.title?.toLowerCase() + if (condition.id === conditionId) { + if (title === 'if') { + return `condition-${blockId}-if` + } else if (title === 'else if') { + elseIfCount++ + return elseIfCount === 1 + ? `condition-${blockId}-else-if` + : `condition-${blockId}-else-if-${elseIfCount}` + } else if (title === 'else') { + return `condition-${blockId}-else` + } + } + // Count else-ifs as we iterate + if (title === 'else if') { + elseIfCount++ + } + } + + // Fallback: return original handle if condition not found + return handle +} + +/** + * Convert internal router handle (router-{uuid}) to semantic format (router-{blockId}-route-N) + */ +function convertRouterHandleToSemantic( + handle: string, + blockId: string, + block: BlockState +): string { + if (!handle.startsWith('router-')) { + return handle + } + + // Extract the route UUID from the handle + const routeId = handle.substring('router-'.length) + + // Get routes from block subBlocks + const routesValue = block.subBlocks?.routes?.value + if (!routesValue || typeof routesValue !== 'string') { + return handle + } + + let routes: Array<{ id: string; title?: string }> + try { + routes = JSON.parse(routesValue) + } catch { + return handle + } + + if (!Array.isArray(routes)) { + return handle + } + + // Find the route by ID and generate semantic handle (1-indexed) + for (let i = 0; i < routes.length; i++) { + if (routes[i].id === routeId) { + return `router-${blockId}-route-${i + 1}` + } + } + + // Fallback: return original handle if route not found + return handle +} + +/** + * Convert source handle to semantic format for condition and router blocks + */ +function convertToSemanticHandle(handle: string, blockId: string, block: BlockState): string { + if (handle.startsWith('condition-') && block.type === 'condition') { + return convertConditionHandleToSemantic(handle, blockId, block) + } + + if (handle.startsWith('router-') && block.type === 'router_v2') { + return convertRouterHandleToSemantic(handle, blockId, block) + } + + return handle +} + /** * Extract connections for a block from edges and format as operations-style connections + * Converts internal UUID handles to semantic format for training data */ function extractConnectionsForBlock( blockId: string, - edges: WorkflowState['edges'] + edges: WorkflowState['edges'], + block: BlockState ): Record | undefined { const connections: Record = {} @@ -284,9 +402,12 @@ function extractConnectionsForBlock( return undefined } - // Group by source handle + // Group by source handle (converting to semantic format) for (const edge of outgoingEdges) { - const handle = edge.sourceHandle || 'source' + let handle = edge.sourceHandle || 'source' + + // Convert internal UUID handles to semantic format + handle = convertToSemanticHandle(handle, blockId, block) if (!connections[handle]) { connections[handle] = [] @@ -321,7 +442,7 @@ export function sanitizeForCopilot(state: WorkflowState): CopilotWorkflowState { // Helper to recursively sanitize a block and its children const sanitizeBlock = (blockId: string, block: BlockState): CopilotBlockState => { - const connections = extractConnectionsForBlock(blockId, state.edges) + const connections = extractConnectionsForBlock(blockId, state.edges, block) // For loop/parallel blocks, extract config from block.data instead of subBlocks let inputs: Record From 8bc1092a687717e20507149c1c82145e4bec55eb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 15:34:56 -0800 Subject: [PATCH 3/6] Fix queue --- .../hooks/use-checkpoint-management.ts | 13 +++---------- .../hooks/use-message-editing.ts | 1 + apps/sim/stores/panel/copilot/store.ts | 19 ++++++++++++------- apps/sim/stores/panel/copilot/types.ts | 1 + 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts index 62606999c6..9dcfe25fa6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts @@ -94,7 +94,6 @@ export function useCheckpointManagement( setShowRestoreConfirmation(false) onRevertModeChange?.(false) - onEditModeChange?.(true) logger.info('Checkpoint reverted and removed from message', { messageId: message.id, @@ -108,15 +107,7 @@ export function useCheckpointManagement( setIsReverting(false) } } - }, [ - messageCheckpoints, - revertToCheckpoint, - message.id, - messages, - currentChat, - onRevertModeChange, - onEditModeChange, - ]) + }, [messageCheckpoints, revertToCheckpoint, message.id, messages, currentChat, onRevertModeChange]) /** * Cancels checkpoint revert @@ -176,6 +167,7 @@ export function useCheckpointManagement( fileAttachments: fileAttachments || message.fileAttachments, contexts: contexts || (message as any).contexts, messageId: message.id, + queueIfBusy: false, }) } pendingEditRef.current = null @@ -219,6 +211,7 @@ export function useCheckpointManagement( fileAttachments: fileAttachments || message.fileAttachments, contexts: contexts || (message as any).contexts, messageId: message.id, + queueIfBusy: false, }) } pendingEditRef.current = null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts index ff69e51272..bdef4401b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts @@ -166,6 +166,7 @@ export function useMessageEditing(props: UseMessageEditingProps) { fileAttachments: fileAttachments || message.fileAttachments, contexts: contexts || (message as any).contexts, messageId: message.id, + queueIfBusy: false, }) } }, diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 67d12aa192..d93cd30af4 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -2562,16 +2562,18 @@ export const useCopilotStore = create()( fileAttachments, contexts, messageId, + queueIfBusy = true, } = options as { stream?: boolean fileAttachments?: MessageFileAttachment[] contexts?: ChatContext[] messageId?: string + queueIfBusy?: boolean } if (!workflowId) return - // If already sending a message, queue this one instead + // If already sending a message, queue this one instead unless bypassing queue if (isSendingMessage && !activeAbortController) { logger.warn('[Copilot] sendMessage: stale sending state detected, clearing', { originalMessageId: messageId, @@ -2583,12 +2585,15 @@ export const useCopilotStore = create()( }) set({ isSendingMessage: false, abortController: null }) } else if (isSendingMessage) { - get().addToQueue(message, { fileAttachments, contexts, messageId }) - logger.info('[Copilot] Message queued (already sending)', { - queueLength: get().messageQueue.length + 1, - originalMessageId: messageId, - }) - return + if (queueIfBusy) { + get().addToQueue(message, { fileAttachments, contexts, messageId }) + logger.info('[Copilot] Message queued (already sending)', { + queueLength: get().messageQueue.length + 1, + originalMessageId: messageId, + }) + return + } + get().abortMessage({ suppressContinueOption: true }) } const nextAbortController = new AbortController() diff --git a/apps/sim/stores/panel/copilot/types.ts b/apps/sim/stores/panel/copilot/types.ts index d0275a633e..477275c3a6 100644 --- a/apps/sim/stores/panel/copilot/types.ts +++ b/apps/sim/stores/panel/copilot/types.ts @@ -179,6 +179,7 @@ export interface CopilotActions { fileAttachments?: MessageFileAttachment[] contexts?: ChatContext[] messageId?: string + queueIfBusy?: boolean } ) => Promise abortMessage: (options?: { suppressContinueOption?: boolean }) => void From fd5695e438020e8c9f9699d1f87640e670de67db Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 15:35:50 -0800 Subject: [PATCH 4/6] Fix lint --- .../hooks/use-checkpoint-management.ts | 9 ++++++++- .../sim/lib/workflows/sanitization/json-sanitizer.ts | 12 +++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts index 9dcfe25fa6..845e8e490b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts @@ -107,7 +107,14 @@ export function useCheckpointManagement( setIsReverting(false) } } - }, [messageCheckpoints, revertToCheckpoint, message.id, messages, currentChat, onRevertModeChange]) + }, [ + messageCheckpoints, + revertToCheckpoint, + message.id, + messages, + currentChat, + onRevertModeChange, + ]) /** * Cancels checkpoint revert diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index b62974f4be..d67b2cd34b 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -307,12 +307,14 @@ function convertConditionHandleToSemantic( if (condition.id === conditionId) { if (title === 'if') { return `condition-${blockId}-if` - } else if (title === 'else if') { + } + if (title === 'else if') { elseIfCount++ return elseIfCount === 1 ? `condition-${blockId}-else-if` : `condition-${blockId}-else-if-${elseIfCount}` - } else if (title === 'else') { + } + if (title === 'else') { return `condition-${blockId}-else` } } @@ -329,11 +331,7 @@ function convertConditionHandleToSemantic( /** * Convert internal router handle (router-{uuid}) to semantic format (router-{blockId}-route-N) */ -function convertRouterHandleToSemantic( - handle: string, - blockId: string, - block: BlockState -): string { +function convertRouterHandleToSemantic(handle: string, blockId: string, block: BlockState): string { if (!handle.startsWith('router-')) { return handle } From cc33ab9c2137c79002b8c1a0470831e34e32c4a9 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 15:43:53 -0800 Subject: [PATCH 5/6] Get block options and config for subflows --- .../tools/server/blocks/get-block-config.ts | 58 +++++++++++++++++++ .../tools/server/blocks/get-block-options.ts | 26 +++++++++ 2 files changed, 84 insertions(+) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts index 60bcad823d..d6e7adb7a8 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts @@ -356,6 +356,64 @@ export const getBlockConfigServerTool: BaseServerTool< const logger = createLogger('GetBlockConfigServerTool') logger.debug('Executing get_block_config', { blockType, operation, trigger }) + if (blockType === 'loop') { + const result = { + blockType, + blockName: 'Loop', + operation, + trigger, + inputs: { + loopType: { + type: 'string', + description: 'Loop type', + options: ['for', 'forEach', 'while', 'doWhile'], + default: 'for', + }, + iterations: { + type: 'number', + description: 'Number of iterations (for loop type "for")', + }, + collection: { + type: 'string', + description: 'Collection to iterate (for loop type "forEach")', + }, + condition: { + type: 'string', + description: 'Loop condition (for loop types "while" and "doWhile")', + }, + }, + outputs: {}, + } + return GetBlockConfigResult.parse(result) + } + + if (blockType === 'parallel') { + const result = { + blockType, + blockName: 'Parallel', + operation, + trigger, + inputs: { + parallelType: { + type: 'string', + description: 'Parallel type', + options: ['count', 'collection'], + default: 'count', + }, + count: { + type: 'number', + description: 'Number of parallel branches (for parallel type "count")', + }, + collection: { + type: 'string', + description: 'Collection to branch over (for parallel type "collection")', + }, + }, + outputs: {}, + } + return GetBlockConfigResult.parse(result) + } + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null const allowedIntegrations = permissionConfig?.allowedIntegrations diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts index 595371b0da..e708d77bbf 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts @@ -21,6 +21,32 @@ export const getBlockOptionsServerTool: BaseServerTool< const logger = createLogger('GetBlockOptionsServerTool') logger.debug('Executing get_block_options', { blockId }) + if (blockId === 'loop') { + const result = { + blockId, + blockName: 'Loop', + operations: [ + { id: 'for', name: 'For', description: 'Run a fixed number of iterations.' }, + { id: 'forEach', name: 'For each', description: 'Iterate over a collection.' }, + { id: 'while', name: 'While', description: 'Repeat while a condition is true.' }, + { id: 'doWhile', name: 'Do while', description: 'Run once, then repeat while a condition is true.' }, + ], + } + return GetBlockOptionsResult.parse(result) + } + + if (blockId === 'parallel') { + const result = { + blockId, + blockName: 'Parallel', + operations: [ + { id: 'count', name: 'Count', description: 'Run a fixed number of parallel branches.' }, + { id: 'collection', name: 'Collection', description: 'Run one branch per collection item.' }, + ], + } + return GetBlockOptionsResult.parse(result) + } + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null const allowedIntegrations = permissionConfig?.allowedIntegrations From 323ead1f64a5af89ff8da5559d8fac3558dde33f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 16 Jan 2026 15:48:56 -0800 Subject: [PATCH 6/6] Lint --- .../copilot/tools/server/blocks/get-block-options.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts index e708d77bbf..e98be96900 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts @@ -29,7 +29,11 @@ export const getBlockOptionsServerTool: BaseServerTool< { id: 'for', name: 'For', description: 'Run a fixed number of iterations.' }, { id: 'forEach', name: 'For each', description: 'Iterate over a collection.' }, { id: 'while', name: 'While', description: 'Repeat while a condition is true.' }, - { id: 'doWhile', name: 'Do while', description: 'Run once, then repeat while a condition is true.' }, + { + id: 'doWhile', + name: 'Do while', + description: 'Run once, then repeat while a condition is true.', + }, ], } return GetBlockOptionsResult.parse(result) @@ -41,7 +45,11 @@ export const getBlockOptionsServerTool: BaseServerTool< blockName: 'Parallel', operations: [ { id: 'count', name: 'Count', description: 'Run a fixed number of parallel branches.' }, - { id: 'collection', name: 'Collection', description: 'Run one branch per collection item.' }, + { + id: 'collection', + name: 'Collection', + description: 'Run one branch per collection item.', + }, ], } return GetBlockOptionsResult.parse(result)