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
26 changes: 2 additions & 24 deletions apps/sim/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,6 @@
animation: dash-animation 1.5s linear infinite !important;
}

/**
* Active block ring animation - cycles through gray tones using box-shadow
*/
@keyframes ring-pulse-colors {
0%,
100% {
box-shadow: 0 0 0 4px var(--surface-14);
}
33% {
box-shadow: 0 0 0 4px var(--surface-12);
}
66% {
box-shadow: 0 0 0 4px var(--surface-15);
}
}

.dark .animate-ring-pulse {
animation: ring-pulse-colors 2s ease-in-out infinite !important;
}

.light .animate-ring-pulse {
animation: ring-pulse-colors 2s ease-in-out infinite !important;
}

/**
* Dark color tokens - single source of truth for all colors (dark-only)
*/
Expand Down Expand Up @@ -135,6 +111,7 @@
--border-strong: #d1d1d1;
--divider: #e5e5e5;
--border-muted: #eeeeee;
--border-success: #d5d5d5;
Comment on lines 111 to +114
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Adding CSS variables to globals.css violates the style guide which states "You should not update the global styles unless it is absolutely necessary." Consider defining --border-success in the Tailwind config or in local component styles instead.

Context Used: Context from dashboard - Avoid editing the globals.css file unless absolutely necessary. Move style changes to local componen... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/app/globals.css
Line: 111:114

Comment:
**style:** Adding CSS variables to `globals.css` violates the style guide which states "You should not update the global styles unless it is absolutely necessary." Consider defining `--border-success` in the Tailwind config or in local component styles instead.

**Context Used:** Context from `dashboard` - Avoid editing the globals.css file unless absolutely necessary. Move style changes to local componen... ([source](https://app.greptile.com/review/custom-context?memory=c3b5e4b0-6580-4307-83aa-ba28f105b3c4))

How can I resolve this? If you propose a fix, please make it concise.


/* Brand & state */
--brand-400: #8e4cfb;
Expand Down Expand Up @@ -250,6 +227,7 @@
--border-strong: #303030;
--divider: #393939;
--border-muted: #424242;
--border-success: #575757;

/* Brand & state */
--brand-400: #8e4cfb;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
handleClick,
hasRing,
ringStyles,
runPathStatus,
} = useBlockCore({ blockId: id, data, isPending })

const currentBlock = currentWorkflow.getBlockById(id)
Expand Down Expand Up @@ -750,21 +751,26 @@ export const WorkflowBlock = memo(function WorkflowBlock({
e.stopPropagation()
}}
>
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: isEnabled ? config.bgColor : 'gray' }}
style={{
backgroundColor: isEnabled ? config.bgColor : 'gray',
}}
>
<config.icon className='h-[16px] w-[16px] text-white' />
</div>
<span
className={cn('truncate font-medium text-[16px]', !isEnabled && 'text-[#808080]')}
className={cn(
'truncate font-medium text-[16px]',
!isEnabled && runPathStatus !== 'success' && 'text-[#808080]'
)}
title={name}
>
{name}
</span>
</div>
<div className='flex flex-shrink-0 items-center gap-2'>
<div className='relative z-10 flex flex-shrink-0 items-center gap-2'>
{isWorkflowSelector && childWorkflowId && (
<>
{typeof childIsDeployed === 'boolean' ? (
Expand Down Expand Up @@ -890,6 +896,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({
</Tooltip.Content>
</Tooltip.Root>
)}
{/* {isActive && (
<div className='mr-[2px] ml-2 flex h-[16px] w-[16px] items-center justify-center'>
<div
className='h-full w-full animate-spin-slow rounded-full border-[2.5px] border-[rgba(255,102,0,0.25)] border-t-[var(--warning)]'
aria-hidden='true'
/>
</div>
)} */}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { X } from 'lucide-react'
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow'
import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
import { useExecutionStore } from '@/stores/execution/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'

interface WorkflowEdgeProps extends EdgeProps {
Expand Down Expand Up @@ -43,6 +44,7 @@ export const WorkflowEdge = ({
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
const lastRunEdges = useExecutionStore((state) => state.lastRunEdges)

const generateEdgeIdentity = (
sourceId: string,
Expand Down Expand Up @@ -78,10 +80,16 @@ export const WorkflowEdge = ({
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'

// Check if this edge was traversed during last execution
const edgeRunStatus = lastRunEdges.get(id)

const getEdgeColor = () => {
if (edgeDiffStatus === 'deleted') return 'var(--text-error)'
if (isErrorEdge) return 'var(--text-error)'
if (edgeDiffStatus === 'new') return 'var(--brand-tertiary)'
// Show run path status if edge was traversed
if (edgeRunStatus === 'success') return 'var(--border-success)'
if (edgeRunStatus === 'error') return 'var(--text-error)'
return 'var(--surface-12)'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useCallback, useMemo } from 'react'
import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useBlockState } from '../components/workflow-block/hooks'
import type { WorkflowBlockProps } from '../components/workflow-block/types'
import { getBlockRingStyles } from '../utils/block-ring-utils'
import { useCurrentWorkflow } from './use-current-workflow'

interface UseBlockCoreOptions {
Expand Down Expand Up @@ -43,60 +43,19 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
}, [blockId, setCurrentBlockId])

// Ring styling based on all states
// Priority: active (animated) > pending > focused > deleted > diff > run path
const { hasRing, ringStyles } = useMemo(() => {
const hasRing =
isActive ||
isPending ||
isFocused ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
isDeletedBlock ||
!!runPathStatus

const ringStyles = cn(
// Executing block: animated ring cycling through gray tones (animation handles all styling)
isActive && 'animate-ring-pulse',
// Non-active states use standard ring utilities
!isActive && hasRing && 'ring-[1.75px]',
// Pending state: warning ring
!isActive && isPending && 'ring-[var(--warning)]',
// Focused (selected) state: brand ring
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
// Deleted state (highest priority after active/pending/focused)
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
// Diff states
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'new' &&
'ring-[#22C55E]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'edited' &&
'ring-[var(--warning)]',
// Run path states (lowest priority - only show if no other states active)
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'success' &&
'ring-[var(--surface-14)]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'error' &&
'ring-[var(--text-error)]'
)

return { hasRing, ringStyles }
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])
// Priority: active (executing) > pending > focused > deleted > diff > run path
const { hasRing, ringClassName: ringStyles } = useMemo(
() =>
getBlockRingStyles({
isActive,
isPending,
isFocused,
isDeletedBlock,
diffStatus,
runPathStatus,
}),
[isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus]
)

return {
// Workflow context
Expand All @@ -116,5 +75,6 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
// Ring styling
hasRing,
ringStyles,
runPathStatus,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function useWorkflowExecution() {
setDebugContext,
setActiveBlocks,
setBlockRunStatus,
setEdgeRunStatus,
} = useExecutionStore()
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const executionStream = useExecutionStream()
Expand Down Expand Up @@ -892,6 +893,12 @@ export function useWorkflowExecution() {
activeBlocksSet.add(data.blockId)
// Create a new Set to trigger React re-render
setActiveBlocks(new Set(activeBlocksSet))

// Track edges that led to this block as soon as execution starts
const incomingEdges = workflowEdges.filter((edge) => edge.target === data.blockId)
incomingEdges.forEach((edge) => {
setEdgeRunStatus(edge.id, 'success')
})
},

onBlockCompleted: (data) => {
Expand All @@ -904,6 +911,8 @@ export function useWorkflowExecution() {
// Track successful block execution in run path
setBlockRunStatus(data.blockId, 'success')

// Edges already tracked in onBlockStarted, no need to track again

// Add to console
addConsole({
input: data.input || {},
Expand Down Expand Up @@ -938,7 +947,6 @@ export function useWorkflowExecution() {

// Track failed block execution in run path
setBlockRunStatus(data.blockId, 'error')

// Add error to console
addConsole({
input: data.input || {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cn } from '@/lib/utils'

export type BlockDiffStatus = 'new' | 'edited' | null | undefined

export type BlockRunPathStatus = 'success' | 'error' | undefined

export interface BlockRingOptions {
isActive: boolean
isPending: boolean
isFocused: boolean
isDeletedBlock: boolean
diffStatus: BlockDiffStatus
runPathStatus: BlockRunPathStatus
}

/**
* Derives visual ring visibility and class names for workflow blocks
* based on execution, focus, diff, deletion, and run-path states.
*/
export function getBlockRingStyles(options: BlockRingOptions): {
hasRing: boolean
ringClassName: string
} {
const { isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus } = options

const hasRing =
isActive ||
isPending ||
isFocused ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
isDeletedBlock ||
!!runPathStatus

const ringClassName = cn(
// Executing block: pulsing success ring with prominent thickness
isActive && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
// Non-active states use standard ring utilities
!isActive && hasRing && 'ring-[1.75px]',
// Pending state: warning ring
!isActive && isPending && 'ring-[var(--warning)]',
// Focused (selected) state: brand ring
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
// Deleted state (highest priority after active/pending/focused)
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
// Diff states
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'new' &&
'ring-[#22C55E]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'edited' &&
'ring-[var(--warning)]',
// Run path states (lowest priority - only show if no other states active)
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'success' &&
'ring-[var(--border-success)]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'error' &&
'ring-[var(--text-error)]'
)

return { hasRing, ringClassName }
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './auto-layout-utils'
export * from './block-ring-utils'
export * from './workflow-execution-utils'
10 changes: 8 additions & 2 deletions apps/sim/stores/execution/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
if (isExecuting) {
set({ autoPanDisabled: false })
// Clear run path when starting a new execution
set({ lastRunPath: new Map() })
set({ lastRunPath: new Map(), lastRunEdges: new Map() })
}
},
setIsDebugging: (isDebugging) => set({ isDebugging }),
Expand All @@ -69,6 +69,12 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
newRunPath.set(blockId, status)
set({ lastRunPath: newRunPath })
},
clearRunPath: () => set({ lastRunPath: new Map() }),
setEdgeRunStatus: (edgeId, status) => {
const { lastRunEdges } = get()
const newRunEdges = new Map(lastRunEdges)
newRunEdges.set(edgeId, status)
set({ lastRunEdges: newRunEdges })
},
clearRunPath: () => set({ lastRunPath: new Map(), lastRunEdges: new Map() }),
reset: () => set(initialState),
}))
Loading