Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ export function useCheckpointManagement(

setShowRestoreConfirmation(false)
onRevertModeChange?.(false)
onEditModeChange?.(true)

logger.info('Checkpoint reverted and removed from message', {
messageId: message.id,
Expand All @@ -115,7 +114,6 @@ export function useCheckpointManagement(
messages,
currentChat,
onRevertModeChange,
onEditModeChange,
])

/**
Expand Down Expand Up @@ -176,6 +174,7 @@ export function useCheckpointManagement(
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
queueIfBusy: false,
})
}
pendingEditRef.current = null
Expand Down Expand Up @@ -219,6 +218,7 @@ export function useCheckpointManagement(
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
queueIfBusy: false,
})
}
pendingEditRef.current = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
queueIfBusy: false,
})
}
},
Expand Down
58 changes: 58 additions & 0 deletions apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 34 additions & 0 deletions apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,40 @@ 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

Expand Down
98 changes: 98 additions & 0 deletions apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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<string>()
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')
*/
Expand Down
Loading