Skip to content

Commit b939801

Browse files
committed
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
2 parents 015948a + 5988cee commit b939801

14 files changed

Lines changed: 951 additions & 53 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
8080
import { useOperationAccess } from '@/hooks/use-operation-access'
8181
import { usePermissionConfig } from '@/hooks/use-permission-config'
8282
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
83+
import { supportsForcedToolUse } from '@/providers/models'
8384
import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils'
8485
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
8586
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -561,10 +562,11 @@ export const ToolInput = memo(function ToolInput({
561562
})
562563
}, [mcpTools, mcpServers])
563564

564-
const modelValue = useSubBlockStore.getState().getValue(blockId, 'model')
565+
const modelValue = useSubBlockStore((state) => state.getValue(blockId, 'model'))
565566
const model = typeof modelValue === 'string' ? modelValue : ''
566567
const provider = model ? getProviderFromModel(model) : ''
567568
const supportsToolControl = provider ? supportsToolUsageControl(provider) : false
569+
const supportsForce = supportsForcedToolUse(model)
568570

569571
const {
570572
filterBlocks,
@@ -1710,12 +1712,16 @@ export const ToolInput = memo(function ToolInput({
17101712
</PopoverItem>
17111713
<PopoverItem
17121714
active={tool.usageControl === 'force'}
1715+
disabled={!supportsForce}
17131716
onClick={() => {
17141717
handleUsageControlChange(toolIndex, 'force')
17151718
setUsageControlPopoverIndex(null)
17161719
}}
17171720
>
1718-
Force <span className='text-[var(--text-tertiary)]'>(always use)</span>
1721+
Force{' '}
1722+
<span className='text-[var(--text-tertiary)]'>
1723+
{supportsForce ? '(always use)' : '(not supported by model)'}
1724+
</span>
17191725
</PopoverItem>
17201726
<PopoverItem
17211727
active={tool.usageControl === 'none'}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
mockEndScopedExecution,
1717
mockExecute,
1818
mockExecuteFromBlock,
19+
mockFindStartBlock,
1920
mockFetch,
2021
mockHandleExecutionCancelledConsole,
2122
mockHandleExecutionErrorConsole,
@@ -101,6 +102,7 @@ const {
101102
mockEndScopedExecution: vi.fn(() => true),
102103
mockExecute: vi.fn(),
103104
mockExecuteFromBlock: vi.fn(),
105+
mockFindStartBlock: vi.fn(() => ({ blockId: 'start' })),
104106
mockFetch: vi.fn(),
105107
mockHandleExecutionCancelledConsole: vi.fn(),
106108
mockHandleExecutionErrorConsole: vi.fn(),
@@ -180,7 +182,7 @@ vi.mock('@/lib/workflows/triggers/triggers', () => ({
180182
EXTERNAL_TRIGGER: 'external-trigger',
181183
},
182184
TriggerUtils: {
183-
findStartBlock: () => ({ blockId: 'start' }),
185+
findStartBlock: mockFindStartBlock,
184186
getTriggerValidationMessage: () => 'Missing trigger',
185187
},
186188
}))
@@ -1097,6 +1099,7 @@ describe('useWorkflowExecution attachment uploads', () => {
10971099
}
10981100
executionStoreState.getLastExecutionSnapshot.mockReturnValueOnce(sourceSnapshot)
10991101
workflowStoreState.edges.push({ source: 'start', target: 'function-1' } as never)
1102+
workflowStoreState.edges.push({ source: 'function-1', target: 'disabledBranch' } as never)
11001103
const currentBlocks = {
11011104
...workflowBlocks,
11021105
'function-1': {
@@ -1106,6 +1109,18 @@ describe('useWorkflowExecution attachment uploads', () => {
11061109
enabled: true,
11071110
subBlocks: { code: { value: 'return "current editor state"' } },
11081111
},
1112+
disabledBranch: {
1113+
id: 'disabledBranch',
1114+
type: 'slack',
1115+
name: 'Disabled Branch',
1116+
enabled: false,
1117+
subBlocks: {},
1118+
},
1119+
disabledTrigger: {
1120+
...workflowBlocks.start,
1121+
id: 'disabledTrigger',
1122+
enabled: false,
1123+
},
11091124
}
11101125
workflowStoreState.getWorkflowState.mockReturnValueOnce({
11111126
blocks: currentBlocks,
@@ -1147,10 +1162,23 @@ describe('useWorkflowExecution attachment uploads', () => {
11471162
...workflowBlocks.start,
11481163
subBlocks: { inputFormat: { value: 'current-editor-state' } },
11491164
},
1165+
disabledBranch: {
1166+
id: 'disabledBranch',
1167+
type: 'slack',
1168+
name: 'Disabled Branch',
1169+
enabled: false,
1170+
subBlocks: {},
1171+
},
1172+
disabledTrigger: {
1173+
...workflowBlocks.start,
1174+
id: 'disabledTrigger',
1175+
enabled: false,
1176+
},
11501177
}
1178+
const currentEdges = [{ source: 'start', target: 'disabledBranch' }]
11511179
workflowStoreState.getWorkflowState.mockReturnValueOnce({
11521180
blocks: currentBlocks,
1153-
edges: [],
1181+
edges: currentEdges,
11541182
loops: {},
11551183
parallels: {},
11561184
})
@@ -1181,12 +1209,16 @@ describe('useWorkflowExecution attachment uploads', () => {
11811209
isClientSession: true,
11821210
workflowStateOverride: {
11831211
blocks: currentBlocks,
1184-
edges: [],
1212+
edges: currentEdges,
11851213
loops: {},
11861214
parallels: {},
11871215
},
11881216
})
11891217
)
1218+
expect(mockResolveStartCandidates).toHaveBeenCalledWith(
1219+
{ start: currentBlocks.start },
1220+
{ execution: 'manual' }
1221+
)
11901222
expect(mockExecute.mock.calls[0]?.[0]).not.toHaveProperty('sourceSnapshot')
11911223
expect(mockExecuteFromBlock).not.toHaveBeenCalled()
11921224
expect(executionStoreState.setLastExecutionSnapshot).toHaveBeenCalledWith(
@@ -1277,3 +1309,102 @@ describe('useWorkflowExecution attachment uploads', () => {
12771309
unmount()
12781310
})
12791311
})
1312+
1313+
describe('useWorkflowExecution workflow state override', () => {
1314+
beforeEach(() => {
1315+
resetWorkflowExecutionTestState()
1316+
vi.stubGlobal('fetch', mockFetch)
1317+
const startCandidate = {
1318+
blockId: 'start',
1319+
block: workflowBlocks.start,
1320+
path: 'legacy-starter',
1321+
}
1322+
mockResolveStartCandidates.mockReturnValue([startCandidate])
1323+
mockSelectBestTrigger.mockReturnValue([startCandidate])
1324+
})
1325+
1326+
afterEach(() => {
1327+
vi.unstubAllGlobals()
1328+
})
1329+
1330+
it.each(['manual', 'chat', 'run-until'] as const)(
1331+
'keeps disabled blocks and edges in %s payloads while excluding disabled triggers',
1332+
async (triggerType) => {
1333+
const blocks = {
1334+
...workflowBlocks,
1335+
condition1: {
1336+
id: 'condition1',
1337+
type: 'condition',
1338+
name: 'Check',
1339+
enabled: true,
1340+
subBlocks: {},
1341+
},
1342+
disabledBranch: {
1343+
id: 'disabledBranch',
1344+
type: 'slack',
1345+
name: 'Send Empty',
1346+
enabled: false,
1347+
subBlocks: {},
1348+
},
1349+
disabledTrigger: {
1350+
...workflowBlocks.start,
1351+
id: 'disabledTrigger',
1352+
enabled: false,
1353+
},
1354+
}
1355+
const edges = [
1356+
{
1357+
id: 'edge-1',
1358+
source: 'condition1',
1359+
target: 'disabledBranch',
1360+
sourceHandle: 'condition-else1',
1361+
},
1362+
]
1363+
workflowStoreState.getWorkflowState.mockReturnValueOnce({
1364+
blocks: { ...blocks, layout: { id: 'layout' } },
1365+
edges,
1366+
loops: {},
1367+
parallels: {},
1368+
})
1369+
const { result, unmount } = renderWorkflowExecutionHook()
1370+
1371+
await act(async () => {
1372+
if (triggerType === 'run-until') {
1373+
await result().handleRunUntilBlock('condition1', 'workflow-1')
1374+
return
1375+
}
1376+
const runResult = await result().handleRunWorkflow(
1377+
triggerType === 'chat'
1378+
? {
1379+
input: 'go',
1380+
conversationId: 'conversation-1',
1381+
}
1382+
: undefined
1383+
)
1384+
await drainStream(runResult)
1385+
})
1386+
1387+
expect(mockExecute).toHaveBeenCalledTimes(1)
1388+
const { workflowStateOverride } = mockExecute.mock.calls[0][0]
1389+
const sentBlockIds = new Set(Object.keys(workflowStateOverride.blocks))
1390+
1391+
expect(workflowStateOverride.blocks).toEqual(blocks)
1392+
expect(workflowStateOverride.edges).toEqual(edges)
1393+
expect(sentBlockIds.has('disabledBranch')).toBe(true)
1394+
for (const edge of workflowStateOverride.edges) {
1395+
expect(sentBlockIds.has(edge.source)).toBe(true)
1396+
expect(sentBlockIds.has(edge.target)).toBe(true)
1397+
}
1398+
const enabledBlocks = { start: blocks.start, condition1: blocks.condition1 }
1399+
if (triggerType === 'chat') {
1400+
expect(mockFindStartBlock).toHaveBeenCalledWith(enabledBlocks, 'chat')
1401+
} else {
1402+
expect(mockResolveStartCandidates).toHaveBeenCalledWith(enabledBlocks, {
1403+
execution: 'manual',
1404+
})
1405+
}
1406+
1407+
unmount()
1408+
}
1409+
)
1410+
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,10 +1104,10 @@ export function useWorkflowExecution() {
11041104
const workflowEdges = (executionWorkflowState?.edges ??
11051105
latestWorkflowState.edges) as typeof currentWorkflow.edges
11061106

1107-
// Filter out blocks without type (these are layout-only blocks) and disabled blocks
1107+
/** Keep disabled targets available for routing; the DAG excludes them from execution. */
11081108
const validBlocks = Object.entries(workflowBlocks).reduce(
11091109
(acc, [blockId, block]) => {
1110-
if (block?.type && block.enabled !== false) {
1110+
if (block?.type) {
11111111
acc[blockId] = block
11121112
}
11131113
return acc
@@ -1145,24 +1145,29 @@ export function useWorkflowExecution() {
11451145
}
11461146
})
11471147

1148-
// Filter out blocks without type and disabled blocks
11491148
const filteredStates = Object.entries(mergedStates).reduce(
11501149
(acc, [id, block]) => {
11511150
if (!block || !block.type) {
11521151
logger.warn(`Skipping block with undefined type: ${id}`, block)
11531152
return acc
11541153
}
1155-
// Skip disabled blocks to prevent them from being passed to executor
1156-
if (block.enabled === false) {
1157-
logger.warn(`Skipping disabled block: ${id}`)
1158-
return acc
1159-
}
11601154
acc[id] = block
11611155
return acc
11621156
},
11631157
{} as typeof mergedStates
11641158
)
11651159

1160+
/** Trigger resolution must never select a disabled trigger. */
1161+
const enabledStates = Object.entries(filteredStates).reduce(
1162+
(acc, [id, block]) => {
1163+
if (block.enabled !== false) {
1164+
acc[id] = block
1165+
}
1166+
return acc
1167+
},
1168+
{} as typeof filteredStates
1169+
)
1170+
11661171
// If this is a chat execution, get the selected outputs
11671172
let selectedOutputs: string[] | undefined
11681173
if (isExecutingFromChat && activeWorkflowId) {
@@ -1177,7 +1182,7 @@ export function useWorkflowExecution() {
11771182

11781183
if (isExecutingFromChat) {
11791184
// For chat execution, find the appropriate chat trigger
1180-
const startBlock = TriggerUtils.findStartBlock(filteredStates, 'chat')
1185+
const startBlock = TriggerUtils.findStartBlock(enabledStates, 'chat')
11811186

11821187
if (!startBlock) {
11831188
throw new WorkflowValidationError(
@@ -1191,7 +1196,7 @@ export function useWorkflowExecution() {
11911196
startBlockId = startBlock.blockId
11921197
} else {
11931198
// Manual execution: detect and group triggers by paths
1194-
const candidates = resolveStartCandidates(filteredStates, {
1199+
const candidates = resolveStartCandidates(enabledStates, {
11951200
execution: 'manual',
11961201
})
11971202

@@ -1203,7 +1208,7 @@ export function useWorkflowExecution() {
12031208
'Workflow Validation'
12041209
)
12051210
logger.error('No trigger blocks found for manual run', {
1206-
allBlockTypes: Object.values(filteredStates).map((b) => b.type),
1211+
allBlockTypes: Object.values(enabledStates).map((b) => b.type),
12071212
})
12081213
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
12091214
throw error
@@ -2034,15 +2039,15 @@ export function useWorkflowExecution() {
20342039
const sourceExecutionId = isTriggerBlock ? undefined : effectiveSnapshot.sourceExecutionId
20352040

20362041
const mergedStates = mergeSubblockState(latestWorkflowState.blocks, workflowId)
2037-
const executableStates = Object.entries(mergedStates).reduce(
2042+
const filteredStates = Object.entries(mergedStates).reduce(
20382043
(states, [id, block]) => {
2039-
if (block?.type && block.enabled !== false) states[id] = block
2044+
if (block?.type) states[id] = block
20402045
return states
20412046
},
20422047
{} as typeof mergedStates
20432048
)
20442049
const workflowStateOverride = workflowStateSchema.parse({
2045-
blocks: executableStates,
2050+
blocks: filteredStates,
20462051
edges: workflowEdges,
20472052
loops: latestWorkflowState.loops,
20482053
parallels: latestWorkflowState.parallels,
@@ -2051,7 +2056,14 @@ export function useWorkflowExecution() {
20512056
// Extract mock payload for trigger blocks
20522057
let workflowInput: any
20532058
if (isTriggerBlock) {
2054-
const candidates = resolveStartCandidates(executableStates, { execution: 'manual' })
2059+
const enabledStates = Object.entries(filteredStates).reduce(
2060+
(states, [id, block]) => {
2061+
if (block.enabled !== false) states[id] = block
2062+
return states
2063+
},
2064+
{} as typeof filteredStates
2065+
)
2066+
const candidates = resolveStartCandidates(enabledStates, { execution: 'manual' })
20552067
const candidate = candidates.find((c) => c.blockId === blockId)
20562068

20572069
if (candidate) {
@@ -2069,7 +2081,7 @@ export function useWorkflowExecution() {
20692081
}
20702082
} else {
20712083
// Fallback: block is trigger by position but not classified as start candidate
2072-
const block = executableStates[blockId]
2084+
const block = enabledStates[blockId]
20732085
if (block) {
20742086
const blockConfig = getBlock(block.type)
20752087
const hasTriggers = blockConfig?.triggers?.available?.length

apps/sim/executor/dag/construction/edges.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export class EdgeConstructor {
7575
const routerV2ConfigMap = new Map<string, RouterV2RouteConfig[]>()
7676

7777
for (const block of workflow.blocks) {
78+
if (block.enabled === false) continue
79+
7880
const blockType = block.metadata?.id ?? ''
7981
blockTypeMap.set(block.id, blockType)
8082

0 commit comments

Comments
 (0)