improvement: runpath edges, blocks, active (#2008)

This commit is contained in:
Emir Karabeg
2025-11-14 23:26:41 -08:00
committed by GitHub
parent f8070f9029
commit 4bd0f31f36
10 changed files with 153 additions and 89 deletions
+2 -24
View File
@@ -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)
*/
@@ -135,6 +111,7 @@
--border-strong: #d1d1d1;
--divider: #e5e5e5;
--border-muted: #eeeeee;
--border-success: #d5d5d5;
/* Brand & state */
--brand-400: #8e4cfb;
@@ -250,6 +227,7 @@
--border-strong: #303030;
--divider: #393939;
--border-muted: #424242;
--border-success: #575757;
/* Brand & state */
--brand-400: #8e4cfb;
@@ -343,6 +343,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
handleClick,
hasRing,
ringStyles,
runPathStatus,
} = useBlockCore({ blockId: id, data, isPending })
const currentBlock = currentWorkflow.getBlockById(id)
@@ -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' ? (
@@ -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>
@@ -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 {
@@ -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,
@@ -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)'
}
@@ -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 {
@@ -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
@@ -116,5 +75,6 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
// Ring styling
hasRing,
ringStyles,
runPathStatus,
}
}
@@ -100,6 +100,7 @@ export function useWorkflowExecution() {
setDebugContext,
setActiveBlocks,
setBlockRunStatus,
setEdgeRunStatus,
} = useExecutionStore()
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const executionStream = useExecutionStream()
@@ -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) => {
@@ -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 || {},
@@ -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 || {},
@@ -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 }
}
@@ -1,2 +1,3 @@
export * from './auto-layout-utils'
export * from './block-ring-utils'
export * from './workflow-execution-utils'
+8 -2
View File
@@ -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 }),
@@ -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),
}))
+13 -1
View File
@@ -6,6 +6,11 @@ import type { ExecutionContext } from '@/executor/types'
*/
export type BlockRunStatus = 'success' | 'error'
/**
* Represents the execution result of an edge in the last run
*/
export type EdgeRunStatus = 'success' | 'error'
export interface ExecutionState {
activeBlockIds: Set<string>
isExecuting: boolean
@@ -16,9 +21,14 @@ export interface ExecutionState {
autoPanDisabled: boolean
/**
* Tracks blocks from the last execution run and their success/error status.
* Cleared when a new run starts. Used to show run path indicators (green/red rings).
* Cleared when a new run starts. Used to show run path indicators (rings on blocks).
*/
lastRunPath: Map<string, BlockRunStatus>
/**
* Tracks edges from the last execution run and their success/error status.
* Cleared when a new run starts. Used to show run path indicators on edges.
*/
lastRunEdges: Map<string, EdgeRunStatus>
}
export interface ExecutionActions {
@@ -30,6 +40,7 @@ export interface ExecutionActions {
setDebugContext: (context: ExecutionContext | null) => void
setAutoPanDisabled: (disabled: boolean) => void
setBlockRunStatus: (blockId: string, status: BlockRunStatus) => void
setEdgeRunStatus: (edgeId: string, status: EdgeRunStatus) => void
clearRunPath: () => void
reset: () => void
}
@@ -43,6 +54,7 @@ export const initialState: ExecutionState = {
debugContext: null,
autoPanDisabled: false,
lastRunPath: new Map(),
lastRunEdges: new Map(),
}
// Types for panning functionality
+2 -2
View File
@@ -144,10 +144,10 @@ export default {
},
'ring-pulse': {
'0%, 100%': {
opacity: '1',
'box-shadow': '0 0 0 1.5px var(--border-success)',
},
'50%': {
opacity: '0.6',
'box-shadow': '0 0 0 4px var(--border-success)',
},
},
},