Skip to content

Commit 643c318

Browse files
committed
improvement(workflow): refine live execution feedback
1 parent ec833c1 commit 643c318

15 files changed

Lines changed: 884 additions & 378 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx

Lines changed: 347 additions & 329 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
advanceActionSweep,
4+
INITIAL_ACTION_SWEEP_STATE,
5+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/use-running-action-sweep'
6+
7+
describe('advanceActionSweep', () => {
8+
it('fills action slots cumulatively from left to right', () => {
9+
const first = advanceActionSweep(INITIAL_ACTION_SWEEP_STATE, 3)
10+
const second = advanceActionSweep(first, 3)
11+
const third = advanceActionSweep(second, 3)
12+
13+
expect([first.filledCount, second.filledCount, third.filledCount]).toEqual([1, 2, 3])
14+
expect(third.direction).toBe(-1)
15+
})
16+
17+
it('empties action slots from right to left before restarting', () => {
18+
const second = advanceActionSweep({ filledCount: 3, direction: -1 }, 3)
19+
const first = advanceActionSweep(second, 3)
20+
const empty = advanceActionSweep(first, 3)
21+
22+
expect([second.filledCount, first.filledCount, empty.filledCount]).toEqual([2, 1, 0])
23+
expect(empty.direction).toBe(1)
24+
})
25+
26+
it('stays empty when there are no action slots', () => {
27+
expect(advanceActionSweep({ filledCount: 2, direction: -1 }, 0)).toEqual(
28+
INITIAL_ACTION_SWEEP_STATE
29+
)
30+
})
31+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useEffect, useState } from 'react'
2+
3+
const ACTION_SWEEP_INTERVAL_MS = 160
4+
5+
interface ActionSweepState {
6+
filledCount: number
7+
direction: 1 | -1
8+
}
9+
10+
export const INITIAL_ACTION_SWEEP_STATE: ActionSweepState = {
11+
filledCount: 0,
12+
direction: 1,
13+
}
14+
15+
/** Advances the cumulative action-slot sweep by one frame. */
16+
export function advanceActionSweep(state: ActionSweepState, slotCount: number): ActionSweepState {
17+
if (slotCount <= 0) return INITIAL_ACTION_SWEEP_STATE
18+
19+
const nextCount = state.filledCount + state.direction
20+
if (nextCount >= slotCount) {
21+
return { filledCount: slotCount, direction: -1 }
22+
}
23+
if (nextCount <= 0) {
24+
return INITIAL_ACTION_SWEEP_STATE
25+
}
26+
return { filledCount: nextCount, direction: state.direction }
27+
}
28+
29+
/** Runs a cumulative left-to-right, right-to-left sweep while a block executes. */
30+
export function useRunningActionSweep(isRunning: boolean, slotCount: number): number {
31+
const [state, setState] = useState<ActionSweepState>(INITIAL_ACTION_SWEEP_STATE)
32+
33+
useEffect(() => {
34+
setState(INITIAL_ACTION_SWEEP_STATE)
35+
if (!isRunning || slotCount <= 0) return
36+
37+
const intervalId = window.setInterval(() => {
38+
setState((current) => advanceActionSweep(current, slotCount))
39+
}, ACTION_SWEEP_INTERVAL_MS)
40+
41+
return () => window.clearInterval(intervalId)
42+
}, [isRunning, slotCount])
43+
44+
return isRunning ? state.filledCount : 0
45+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hoo
1717
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
1818
import { isBlockProtected } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
1919
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
20+
import { useIsCurrentWorkflowExecuting } from '@/stores/execution'
2021
import { usePanelEditorStore } from '@/stores/panel'
2122
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
2223
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -87,6 +88,7 @@ export const NoteBlock = memo(function NoteBlock({
8788
const noteColor = isNoteColor(rawColor) ? rawColor : DEFAULT_NOTE_COLOR
8889

8990
const userPermissions = useUserPermissionsContext()
91+
const isWorkflowRunning = useIsCurrentWorkflowExecuting()
9092
const canEditWorkflow = userPermissions.canEdit && !data.isWorkflowLocked
9193
const isProtected = useWorkflowStore(
9294
useCallback((state) => isBlockProtected(id, state.blocks), [id])
@@ -198,6 +200,7 @@ export const NoteBlock = memo(function NoteBlock({
198200
blockType={type}
199201
disabled={!canEditWorkflow}
200202
variant='swell'
203+
isWorkflowRunning={isWorkflowRunning}
201204
noteColor={noteColor}
202205
onNoteColorChange={handleColorChange}
203206
onNoteColorMenuOpen={handleNoteSelect}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { type NodeProps, useReactFlow } from 'reactflow'
44
import { hasDiffStatus } from '@/lib/workflows/diff/types'
55
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
66
import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar'
7-
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
8-
import { useIsCurrentWorkflowExecuting } from '@/stores/execution'
7+
import {
8+
useCurrentWorkflow,
9+
useIsBlockInActiveExecutionHandoff,
10+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
11+
import { useIsBlockActive, useIsCurrentWorkflowExecuting } from '@/stores/execution'
912
import { usePanelEditorStore } from '@/stores/panel'
1013

1114
/**
@@ -35,6 +38,8 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
3538
const isFocused = currentBlockId === id
3639

3740
const isWorkflowRunning = useIsCurrentWorkflowExecuting()
41+
const isRunning = useIsBlockActive(id)
42+
const isExecutionHighlighted = useIsBlockInActiveExecutionHandoff(id)
3843

3944
/**
4045
* Nesting depth, walking the parent chain so the view can apply nested
@@ -62,7 +67,9 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
6267
isEnabled={isEnabled}
6368
isLocked={isLocked}
6469
isFocused={isFocused}
65-
isRunning={isWorkflowRunning}
70+
isRunning={isRunning}
71+
isWorkflowRunning={isWorkflowRunning}
72+
isExecutionHighlighted={isExecutionHighlighted}
6673
diffStatus={diffStatus}
6774
nestingLevel={nestingLevel}
6875
canEditWorkflow={canEditWorkflow}
@@ -73,7 +80,8 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
7380
blockType={data.kind}
7481
disabled={!canEditWorkflow}
7582
variant='swell'
76-
isRunning={isWorkflowRunning}
83+
isRunning={isRunning}
84+
isWorkflowRunning={isWorkflowRunning}
7785
/>
7886
}
7987
/>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ import {
6868
getProviderName,
6969
shouldSkipBlockRender,
7070
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
71-
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
71+
import {
72+
useBlockVisual,
73+
useIsBlockInActiveExecutionHandoff,
74+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
7275
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
7376
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
7477
import { getBlock } from '@/blocks/registry'
@@ -781,6 +784,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
781784
currentWorkflow,
782785
activeWorkflowId,
783786
isEnabled,
787+
isExecuting,
784788
isLocked,
785789
handleClick,
786790
hasRing,
@@ -789,6 +793,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
789793
} = useBlockVisual({ blockId: id, data, isPending, isSelected: selected })
790794

791795
const isWorkflowRunning = useIsCurrentWorkflowExecuting()
796+
const isExecutionHighlighted = useIsBlockInActiveExecutionHandoff(id)
792797
const currentWorkflowId = (params.workflowId as string) || activeWorkflowId || ''
793798

794799
const currentBlock = currentWorkflow.getBlockById(id)
@@ -1402,7 +1407,9 @@ export const WorkflowBlock = memo(function WorkflowBlock({
14021407
hasRing={hasRing}
14031408
ringStyles={ringStyles}
14041409
runPathStatus={runPathStatus}
1405-
isRunning={isWorkflowRunning}
1410+
isRunning={isExecuting}
1411+
isWorkflowRunning={isWorkflowRunning}
1412+
isExecutionHighlighted={isExecutionHighlighted}
14061413
Icon={config.icon}
14071414
iconBgColor={config.bgColor}
14081415
isIntegration={config.category === 'tools'}
@@ -1457,7 +1464,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
14571464
blockType={type}
14581465
disabled={!canEditWorkflow}
14591466
variant='swell'
1460-
isRunning={isWorkflowRunning}
1467+
isRunning={isExecuting}
1468+
isWorkflowRunning={isWorkflowRunning}
14611469
/>
14621470
) : undefined
14631471
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { memo, useCallback, useMemo } from 'react'
22
import { type EdgeDiffStatus, WorkflowEdgeView } from '@sim/workflow-renderer'
33
import { type EdgeProps, useStore } from 'reactflow'
44
import { useShallow } from 'zustand/react/shallow'
5-
import { useIsCurrentWorkflowExecuting, useLastRunEdges } from '@/stores/execution'
5+
import {
6+
useIsBlockActive,
7+
useIsCurrentWorkflowExecuting,
8+
useLastRunEdges,
9+
} from '@/stores/execution'
610
import { usePanelEditorStore, usePanelStore } from '@/stores/panel'
711
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
812

@@ -30,6 +34,7 @@ const WorkflowEdgeComponent = (props: WorkflowEdgeProps) => {
3034
)
3135
const lastRunEdges = useLastRunEdges()
3236
const isWorkflowRunning = useIsCurrentWorkflowExecuting()
37+
const isTargetActive = useIsBlockActive(target)
3338
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
3439
const activeTab = usePanelStore((state) => state.activeTab)
3540

@@ -94,6 +99,7 @@ const WorkflowEdgeComponent = (props: WorkflowEdgeProps) => {
9499
runStatus={runStatus}
95100
isPreviewRun={Boolean(previewExecutionStatus)}
96101
isWorkflowRunning={isWorkflowRunning}
102+
isTargetActive={isTargetActive}
97103
isConnectedToSelection={shouldHighlightEdge}
98104
/>
99105
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export { useBlockVisual } from './use-block-visual'
77
export { useCanvasContextMenu } from './use-canvas-context-menu'
88
export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow'
99
export { useDynamicHandleRefresh } from './use-dynamic-handle-refresh'
10+
export {
11+
isBlockInActiveExecutionHandoff,
12+
useIsBlockInActiveExecutionHandoff,
13+
} from './use-execution-handoff'
1014
export { useNodeUtilities } from './use-node-utilities'
1115
export { usePreventZoom } from './use-prevent-zoom'
1216
export { useScrollManagement } from './use-scroll-management'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { isBlockInActiveExecutionHandoff } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-execution-handoff'
3+
4+
const edges = [
5+
{ id: 'source-to-target', source: 'source', target: 'target' },
6+
{ id: 'stale-to-target', source: 'stale-source', target: 'target' },
7+
{ id: 'source-to-idle', source: 'source', target: 'idle-target' },
8+
]
9+
10+
describe('isBlockInActiveExecutionHandoff', () => {
11+
it('highlights the active destination and the source that just delivered it', () => {
12+
const options = {
13+
isExecuting: true,
14+
activeBlockIds: new Set(['target']),
15+
lastRunEdges: new Map([['source-to-target', 'success']]),
16+
edges,
17+
}
18+
19+
expect(isBlockInActiveExecutionHandoff({ ...options, blockId: 'target' })).toBe(true)
20+
expect(isBlockInActiveExecutionHandoff({ ...options, blockId: 'source' })).toBe(true)
21+
})
22+
23+
it('does not highlight unrelated sources or previously traversed inactive paths', () => {
24+
const options = {
25+
isExecuting: true,
26+
activeBlockIds: new Set(['target']),
27+
lastRunEdges: new Map([
28+
['source-to-target', 'success'],
29+
['source-to-idle', 'success'],
30+
]),
31+
edges,
32+
}
33+
34+
expect(isBlockInActiveExecutionHandoff({ ...options, blockId: 'stale-source' })).toBe(false)
35+
expect(isBlockInActiveExecutionHandoff({ ...options, blockId: 'idle-target' })).toBe(false)
36+
})
37+
38+
it('clears every handoff highlight when execution stops', () => {
39+
expect(
40+
isBlockInActiveExecutionHandoff({
41+
blockId: 'target',
42+
isExecuting: false,
43+
activeBlockIds: new Set(['target']),
44+
lastRunEdges: new Map([['source-to-target', 'success']]),
45+
edges,
46+
})
47+
).toBe(false)
48+
})
49+
50+
it('recomputes the handoff when the execution snapshot changes', () => {
51+
expect(
52+
isBlockInActiveExecutionHandoff({
53+
blockId: 'source',
54+
isExecuting: true,
55+
activeBlockIds: new Set(['target']),
56+
lastRunEdges: new Map([['source-to-target', 'success']]),
57+
edges,
58+
})
59+
).toBe(true)
60+
61+
expect(
62+
isBlockInActiveExecutionHandoff({
63+
blockId: 'source',
64+
isExecuting: true,
65+
activeBlockIds: new Set(['idle-target']),
66+
lastRunEdges: new Map(),
67+
edges,
68+
})
69+
).toBe(false)
70+
})
71+
})

0 commit comments

Comments
 (0)