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
41 changes: 41 additions & 0 deletions src/components/nodes/BaseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,22 @@ export default function BaseNode({
// Test result for this node (keyed by label)
const testResult = useWorkflowStore((s) => s.testResults[label]);

// Pipeline name for step nodes (shows which pipeline a step belongs to).
// Short-circuits for non-step.* modules to avoid an O(n) scan on every store update.
const pipelineName = useWorkflowStore((s) => {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

pipelineName is derived via s.nodes.find(...) for every rendered node. This makes BaseNode rendering O(n²) across the graph (each node component scans the full nodes array), and the selector work also runs on every store update. Consider avoiding the linear search (e.g., pass pipelineName through node props, or add/maintain an id→node map in the store and select from that, or at least short-circuit for non-step.* nodes).

Suggested change
const pipelineName = useWorkflowStore((s) => {
const pipelineName = useWorkflowStore((s) => {
// Only step.* modules participate in pipelines; avoid scanning s.nodes for others.
if (!moduleType.startsWith('step.')) {
return undefined;
}

Copilot uses AI. Check for mistakes.
if (!moduleType.startsWith('step.')) {
return undefined;
}
const node = s.nodes.find((n) => n.id === id);
const handlerId = node?.data.pipelineHandlerId as string | undefined;
if (handlerId) {
// Resolve current label of the handler node (stable reference avoids stale copies on rename)
const handlerNode = s.nodes.find((n) => n.id === handlerId);
return handlerNode?.data.label;
}
return node?.data.pipelineName as string | undefined;
});

// Source file badge (multi-file configs)
const sourceMap = useWorkflowStore((s) => s.sourceMap);
const hasMultipleSourceFiles = useMemo(() => new Set(sourceMap.values()).size > 1, [sourceMap]);
Expand Down Expand Up @@ -417,6 +433,31 @@ export default function BaseNode({
)}
</div>

{pipelineName && (
<div
style={{
borderBottom: `1px solid ${color}20`,
padding: '2px 10px',
background: `${color}10`,
}}
>
<span
style={{
fontSize: 9,
color: '#a6adc8',
fontStyle: 'italic',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
}}
title={`Pipeline: ${pipelineName}`}
>
{pipelineName}
</span>
</div>
)}

<div style={{ padding: '6px 10px' }}>
{ioSig && ioSig.inputs.length > 0 && (
<IOPortList ports={ioSig.inputs} direction="in" color={color} />
Expand Down
4 changes: 4 additions & 0 deletions src/stores/workflowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export interface WorkflowNodeData extends Record<string, unknown> {
synthesized?: boolean;
/** Source file path this node originated from (multi-file configs) */
sourceFile?: string;
/** Pipeline name this step node belongs to (pipeline-only partial configs) */
pipelineName?: string;
/** Handler node ID for route-attached pipeline step nodes; resolved to the handler's current label at render time to avoid stale copies */
pipelineHandlerId?: string;
handlerRoutes?: Array<{
method: string;
path: string;
Expand Down
Loading
Loading