Browse Source

feat: add skipped node visual treatment during execution

- Add node-skipped CSS class (opacity 0.35) for skip dimming
- Expose skippedNodeIds in Zustand store for reactive CSS updates
- Apply node-skipped class in WorkflowCanvas alongside switch-dimmed
- Clear skippedNodeIds on workflow stop, reset, and completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
1b713d72a7
  1. 8
      src/app/globals.css
  2. 19
      src/components/WorkflowCanvas.tsx
  3. 18
      src/store/workflowStore.ts

8
src/app/globals.css

@ -68,7 +68,13 @@ body {
transition: opacity 250ms ease-in-out;
}
.react-flow__node:not(.switch-dimmed) {
/* Skip dimming - nodes skipped due to optional empty inputs */
.react-flow__node.node-skipped {
opacity: 0.35;
transition: opacity 250ms ease-in-out;
}
.react-flow__node:not(.switch-dimmed):not(.node-skipped) {
transition: opacity 250ms ease-in-out;
}

19
src/components/WorkflowCanvas.tsx

@ -272,7 +272,7 @@ export const isPanningRef = { current: false };
export const isDraggingNodeRef = { current: false };
export function WorkflowCanvas() {
const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds } =
const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds, skippedNodeIds } =
useWorkflowStore(useShallow((state) => ({
nodes: state.nodes,
edges: state.edges,
@ -282,6 +282,7 @@ export function WorkflowCanvas() {
navigationTarget: state.navigationTarget,
canvasNavigationSettings: state.canvasNavigationSettings,
dimmedNodeIds: state.dimmedNodeIds,
skippedNodeIds: state.skippedNodeIds,
})));
const onNodesChange = useWorkflowStore((state) => state.onNodesChange);
const onEdgesChange = useWorkflowStore((state) => state.onEdgesChange);
@ -337,24 +338,28 @@ export function WorkflowCanvas() {
}
}, [navigationTarget, nodes, setCenter, setNavigationTarget]);
// Apply dimming className to nodes downstream of disabled Switch outputs
// Apply dimming className to nodes downstream of disabled Switch outputs or skipped by optional inputs
const allNodes = useMemo(() => {
return nodes.map((node) => {
// Never dim Switch or ConditionalSwitch nodes themselves
if (node.type === "switch" || node.type === "conditionalSwitch") return node;
const isDimmed = dimmedNodeIds.has(node.id);
const dimClass = isDimmed ? "switch-dimmed" : "";
const isSkipped = skippedNodeIds.has(node.id);
const extraClasses = [
isDimmed ? "switch-dimmed" : "",
isSkipped ? "node-skipped" : "",
].filter(Boolean).join(" ");
// Preserve existing className if any, add/remove dimmed class
const baseClass = (node.className || "").replace(/\bswitch-dimmed\b/g, "").trim();
const newClass = dimClass ? `${baseClass} ${dimClass}`.trim() : baseClass;
// Preserve existing className if any, add/remove dimmed/skipped classes
const baseClass = (node.className || "").replace(/\bswitch-dimmed\b/g, "").replace(/\bnode-skipped\b/g, "").trim();
const newClass = extraClasses ? `${baseClass} ${extraClasses}`.trim() : baseClass;
// Only create new node object if className changed
if (node.className === newClass) return node;
return { ...node, className: newClass };
});
}, [nodes, dimmedNodeIds]);
}, [nodes, dimmedNodeIds, skippedNodeIds]);
// Node title mapping for FloatingNodeHeaders
const NODE_TITLES: Record<string, string> = {

18
src/store/workflowStore.ts

@ -373,6 +373,9 @@ interface WorkflowStore {
// Switch dimming state
dimmedNodeIds: Set<string>;
// Skip propagation state (optional empty inputs)
skippedNodeIds: Set<string>;
// Switch dimming actions
recomputeDimmedNodes: () => void;
@ -507,6 +510,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
// Switch dimming initial state
dimmedNodeIds: new Set<string>(),
// Skip propagation initial state
skippedNodeIds: new Set<string>(),
setEdgeStyle: (style: EdgeStyle) => {
set({ edgeStyle: style });
},
@ -999,7 +1005,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
const abortController = new AbortController();
const isResuming = startFromNodeId === get().pausedAtNodeId;
const skippedNodeIds = new Set<string>();
set({ isRunning: true, pausedAtNodeId: null, currentNodeIds: [], _abortController: abortController });
set({ isRunning: true, pausedAtNodeId: null, currentNodeIds: [], skippedNodeIds: new Set(), _abortController: abortController });
// Start logging session
await logger.startSession();
@ -1081,6 +1087,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
(node.type === "prompt" && !(nodeData.prompt as string)?.trim());
if (isEmpty) {
skippedNodeIds.add(node.id);
set({ skippedNodeIds: new Set(skippedNodeIds) });
logger.info('node.execution', 'Node skipped (optional input empty)', {
nodeId: node.id,
nodeType: node.type,
@ -1094,6 +1101,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
const hasSkippedSource = incomingEdgesForSkip.some((e) => skippedNodeIds.has(e.source));
if (hasSkippedSource) {
skippedNodeIds.add(node.id);
set({ skippedNodeIds: new Set(skippedNodeIds) });
if (nodeData.status !== undefined) {
get().updateNodeData(node.id, { status: "skipped" } as Partial<WorkflowNodeData>);
}
@ -1255,7 +1263,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
}
}
set({ isRunning: false, currentNodeIds: [], _abortController: null });
set({ isRunning: false, currentNodeIds: [], skippedNodeIds: new Set(), _abortController: null });
saveLogSession();
await logger.endSession();
@ -1278,7 +1286,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
get().updateNodeData(skippedId, { status: "idle" } as Partial<WorkflowNodeData>);
}
}
set({ isRunning: false, currentNodeIds: [], _abortController: null });
set({ isRunning: false, currentNodeIds: [], skippedNodeIds: new Set(), _abortController: null });
saveLogSession();
await logger.endSession();
@ -1291,7 +1299,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
if (controller) {
controller.abort("user-cancelled");
}
set({ isRunning: false, currentNodeIds: [], _abortController: null });
set({ isRunning: false, currentNodeIds: [], skippedNodeIds: new Set(), _abortController: null });
},
setMaxConcurrentCalls: (value: number) => {
@ -1826,6 +1834,8 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
viewedCommentNodeIds: new Set<string>(),
// Reset dimmed nodes
dimmedNodeIds: new Set<string>(),
// Reset skipped nodes
skippedNodeIds: new Set<string>(),
});
get().clearSnapshot();
},

Loading…
Cancel
Save