Skip to content

Commit aa80116

Browse files
authored
fix(copilot): copilot edit router block accepts semantic handles (#2857)
* Fix copilot diff controls * Fix router block for copilot * Fix queue * Fix lint * Get block options and config for subflows * Lint
1 parent 78e4ca9 commit aa80116

File tree

9 files changed

+338
-14
lines changed

9 files changed

+338
-14
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useNotificationStore } from '@/stores/notifications'
88
import { useCopilotStore, usePanelStore } from '@/stores/panel'
99
import { useTerminalStore } from '@/stores/terminal'
1010
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
11+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1112

1213
const logger = createLogger('DiffControls')
1314
const NOTIFICATION_WIDTH = 240
@@ -37,8 +38,15 @@ export const DiffControls = memo(function DiffControls() {
3738
)
3839
)
3940

41+
const { activeWorkflowId } = useWorkflowRegistry(
42+
useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), [])
43+
)
44+
4045
const allNotifications = useNotificationStore((state) => state.notifications)
41-
const hasVisibleNotifications = allNotifications.length > 0
46+
const hasVisibleNotifications = useMemo(() => {
47+
if (!activeWorkflowId) return false
48+
return allNotifications.some((n) => !n.workflowId || n.workflowId === activeWorkflowId)
49+
}, [allNotifications, activeWorkflowId])
4250

4351
const handleAccept = useCallback(() => {
4452
logger.info('Accepting proposed changes with backup protection')

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ export function useCheckpointManagement(
9494

9595
setShowRestoreConfirmation(false)
9696
onRevertModeChange?.(false)
97-
onEditModeChange?.(true)
9897

9998
logger.info('Checkpoint reverted and removed from message', {
10099
messageId: message.id,
@@ -115,7 +114,6 @@ export function useCheckpointManagement(
115114
messages,
116115
currentChat,
117116
onRevertModeChange,
118-
onEditModeChange,
119117
])
120118

121119
/**
@@ -176,6 +174,7 @@ export function useCheckpointManagement(
176174
fileAttachments: fileAttachments || message.fileAttachments,
177175
contexts: contexts || (message as any).contexts,
178176
messageId: message.id,
177+
queueIfBusy: false,
179178
})
180179
}
181180
pendingEditRef.current = null
@@ -219,6 +218,7 @@ export function useCheckpointManagement(
219218
fileAttachments: fileAttachments || message.fileAttachments,
220219
contexts: contexts || (message as any).contexts,
221220
messageId: message.id,
221+
queueIfBusy: false,
222222
})
223223
}
224224
pendingEditRef.current = null

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
166166
fileAttachments: fileAttachments || message.fileAttachments,
167167
contexts: contexts || (message as any).contexts,
168168
messageId: message.id,
169+
queueIfBusy: false,
169170
})
170171
}
171172
},

apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,64 @@ export const getBlockConfigServerTool: BaseServerTool<
356356
const logger = createLogger('GetBlockConfigServerTool')
357357
logger.debug('Executing get_block_config', { blockType, operation, trigger })
358358

359+
if (blockType === 'loop') {
360+
const result = {
361+
blockType,
362+
blockName: 'Loop',
363+
operation,
364+
trigger,
365+
inputs: {
366+
loopType: {
367+
type: 'string',
368+
description: 'Loop type',
369+
options: ['for', 'forEach', 'while', 'doWhile'],
370+
default: 'for',
371+
},
372+
iterations: {
373+
type: 'number',
374+
description: 'Number of iterations (for loop type "for")',
375+
},
376+
collection: {
377+
type: 'string',
378+
description: 'Collection to iterate (for loop type "forEach")',
379+
},
380+
condition: {
381+
type: 'string',
382+
description: 'Loop condition (for loop types "while" and "doWhile")',
383+
},
384+
},
385+
outputs: {},
386+
}
387+
return GetBlockConfigResult.parse(result)
388+
}
389+
390+
if (blockType === 'parallel') {
391+
const result = {
392+
blockType,
393+
blockName: 'Parallel',
394+
operation,
395+
trigger,
396+
inputs: {
397+
parallelType: {
398+
type: 'string',
399+
description: 'Parallel type',
400+
options: ['count', 'collection'],
401+
default: 'count',
402+
},
403+
count: {
404+
type: 'number',
405+
description: 'Number of parallel branches (for parallel type "count")',
406+
},
407+
collection: {
408+
type: 'string',
409+
description: 'Collection to branch over (for parallel type "collection")',
410+
},
411+
},
412+
outputs: {},
413+
}
414+
return GetBlockConfigResult.parse(result)
415+
}
416+
359417
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
360418
const allowedIntegrations = permissionConfig?.allowedIntegrations
361419

apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,40 @@ export const getBlockOptionsServerTool: BaseServerTool<
2121
const logger = createLogger('GetBlockOptionsServerTool')
2222
logger.debug('Executing get_block_options', { blockId })
2323

24+
if (blockId === 'loop') {
25+
const result = {
26+
blockId,
27+
blockName: 'Loop',
28+
operations: [
29+
{ id: 'for', name: 'For', description: 'Run a fixed number of iterations.' },
30+
{ id: 'forEach', name: 'For each', description: 'Iterate over a collection.' },
31+
{ id: 'while', name: 'While', description: 'Repeat while a condition is true.' },
32+
{
33+
id: 'doWhile',
34+
name: 'Do while',
35+
description: 'Run once, then repeat while a condition is true.',
36+
},
37+
],
38+
}
39+
return GetBlockOptionsResult.parse(result)
40+
}
41+
42+
if (blockId === 'parallel') {
43+
const result = {
44+
blockId,
45+
blockName: 'Parallel',
46+
operations: [
47+
{ id: 'count', name: 'Count', description: 'Run a fixed number of parallel branches.' },
48+
{
49+
id: 'collection',
50+
name: 'Collection',
51+
description: 'Run one branch per collection item.',
52+
},
53+
],
54+
}
55+
return GetBlockOptionsResult.parse(result)
56+
}
57+
2458
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
2559
const allowedIntegrations = permissionConfig?.allowedIntegrations
2660

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,25 @@ function validateSourceHandleForBlock(
878878
error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, ${EDGE.ROUTER_PREFIX}{targetId}, error`,
879879
}
880880

881+
case 'router_v2': {
882+
if (!sourceHandle.startsWith(EDGE.ROUTER_PREFIX)) {
883+
return {
884+
valid: false,
885+
error: `Invalid source handle "${sourceHandle}" for router_v2 block. Must start with "${EDGE.ROUTER_PREFIX}"`,
886+
}
887+
}
888+
889+
const routesValue = sourceBlock?.subBlocks?.routes?.value
890+
if (!routesValue) {
891+
return {
892+
valid: false,
893+
error: `Invalid router handle "${sourceHandle}" - no routes defined`,
894+
}
895+
}
896+
897+
return validateRouterHandle(sourceHandle, sourceBlock.id, routesValue)
898+
}
899+
881900
default:
882901
if (sourceHandle === 'source') {
883902
return { valid: true }
@@ -963,6 +982,85 @@ function validateConditionHandle(
963982
}
964983
}
965984

985+
/**
986+
* Validates router handle references a valid route in the block.
987+
* Accepts both internal IDs (router-{routeId}) and semantic keys (router-{blockId}-route-1)
988+
*/
989+
function validateRouterHandle(
990+
sourceHandle: string,
991+
blockId: string,
992+
routesValue: string | any[]
993+
): EdgeHandleValidationResult {
994+
let routes: any[]
995+
if (typeof routesValue === 'string') {
996+
try {
997+
routes = JSON.parse(routesValue)
998+
} catch {
999+
return {
1000+
valid: false,
1001+
error: `Cannot validate router handle "${sourceHandle}" - routes is not valid JSON`,
1002+
}
1003+
}
1004+
} else if (Array.isArray(routesValue)) {
1005+
routes = routesValue
1006+
} else {
1007+
return {
1008+
valid: false,
1009+
error: `Cannot validate router handle "${sourceHandle}" - routes is not an array`,
1010+
}
1011+
}
1012+
1013+
if (!Array.isArray(routes) || routes.length === 0) {
1014+
return {
1015+
valid: false,
1016+
error: `Invalid router handle "${sourceHandle}" - no routes defined`,
1017+
}
1018+
}
1019+
1020+
const validHandles = new Set<string>()
1021+
const semanticPrefix = `router-${blockId}-`
1022+
1023+
for (let i = 0; i < routes.length; i++) {
1024+
const route = routes[i]
1025+
1026+
// Accept internal ID format: router-{uuid}
1027+
if (route.id) {
1028+
validHandles.add(`router-${route.id}`)
1029+
}
1030+
1031+
// Accept 1-indexed route number format: router-{blockId}-route-1, router-{blockId}-route-2, etc.
1032+
validHandles.add(`${semanticPrefix}route-${i + 1}`)
1033+
1034+
// Accept normalized title format: router-{blockId}-{normalized-title}
1035+
// Normalize: lowercase, replace spaces with dashes, remove special chars
1036+
if (route.title && typeof route.title === 'string') {
1037+
const normalizedTitle = route.title
1038+
.toLowerCase()
1039+
.replace(/\s+/g, '-')
1040+
.replace(/[^a-z0-9-]/g, '')
1041+
if (normalizedTitle) {
1042+
validHandles.add(`${semanticPrefix}${normalizedTitle}`)
1043+
}
1044+
}
1045+
}
1046+
1047+
if (validHandles.has(sourceHandle)) {
1048+
return { valid: true }
1049+
}
1050+
1051+
const validOptions = Array.from(validHandles).slice(0, 5)
1052+
const moreCount = validHandles.size - validOptions.length
1053+
let validOptionsStr = validOptions.join(', ')
1054+
if (moreCount > 0) {
1055+
validOptionsStr += `, ... and ${moreCount} more`
1056+
}
1057+
1058+
return {
1059+
valid: false,
1060+
error: `Invalid router handle "${sourceHandle}". Valid handles: ${validOptionsStr}`,
1061+
}
1062+
}
1063+
9661064
/**
9671065
* Validates target handle is valid (must be 'target')
9681066
*/

0 commit comments

Comments
 (0)