Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";

import type { GenericNodeData } from "./GenericAgentNode";

export type ArtifactNodeType = Node<GenericNodeData>;

const ArtifactNode: React.FC<NodeProps<ArtifactNodeType>> = ({ data, id }) => {
return (
<div
className="cursor-pointer rounded-lg border-2 border-purple-600 bg-white px-3 py-3 text-gray-800 shadow-md transition-all duration-200 ease-in-out hover:scale-105 hover:shadow-xl dark:border-purple-400 dark:bg-gray-800 dark:text-gray-200"
style={{ minWidth: "120px", textAlign: "center" }}
>
<Handle type="target" position={Position.Left} id={`${id}-artifact-left-input`} className="!bg-purple-500" isConnectable={true} />
<div className="flex flex-col items-center justify-center gap-1">
<div className="flex items-center justify-center">
<div className="mr-2 h-2 w-2 rounded-full bg-purple-500" />
<div className="text-md">{data.label}</div>
</div>
</div>
</div>
);
};

export default ArtifactNode;
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const GenericToolNode: React.FC<NodeProps<GenericToolNodeType>> = ({ data, id })
</div>
</div>
<Handle type="source" position={Position.Left} id={`${id}-tool-bottom-output`} className="!bg-cyan-500" isConnectable={true} style={{ top: "75%" }} />
<Handle type="source" position={Position.Right} id={`${id}-tool-right-output-artifact`} className="!bg-cyan-500" isConnectable={true} style={{ top: "50%" }} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,18 @@ import {
import { EdgeAnimationService } from "./edgeAnimationService";

// Relevant step types that should be processed in the flow chart
const RELEVANT_STEP_TYPES = ["USER_REQUEST", "AGENT_LLM_CALL", "AGENT_LLM_RESPONSE_TO_AGENT", "AGENT_LLM_RESPONSE_TOOL_DECISION", "AGENT_TOOL_INVOCATION_START", "AGENT_TOOL_EXECUTION_RESULT", "AGENT_RESPONSE_TEXT", "TASK_COMPLETED", "TASK_FAILED"];
const RELEVANT_STEP_TYPES = [
"USER_REQUEST",
"AGENT_LLM_CALL",
"AGENT_LLM_RESPONSE_TO_AGENT",
"AGENT_LLM_RESPONSE_TOOL_DECISION",
"AGENT_TOOL_INVOCATION_START",
"AGENT_TOOL_EXECUTION_RESULT",
"AGENT_RESPONSE_TEXT",
"AGENT_ARTIFACT_NOTIFICATION",
"TASK_COMPLETED",
"TASK_FAILED",
];

interface FlowData {
nodes: Node[];
Expand Down Expand Up @@ -434,6 +445,119 @@ function handleToolExecutionResult(step: VisualizerStep, manager: TimelineLayout
}
}

function handleArtifactNotification(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void {
const currentPhase = getCurrentPhase(manager);
if (!currentPhase) return;

const artifactData = step.data.artifactNotification;
const artifactName = artifactData?.artifactName || "Unnamed Artifact";

const subflow = resolveSubflowContext(manager, step);

// Find the tool node that created this artifact
// We look for a tool that was invoked with the same functionCallId or recently executed
const context = subflow || currentPhase;
let sourceToolNode: NodeInstance | undefined;

if (step.functionCallId) {
sourceToolNode = findToolInstanceByNameEnhanced(context.toolInstances, "", nodes, step.functionCallId) ?? undefined;
}

if (!sourceToolNode && context.toolInstances.length > 0) {
sourceToolNode = context.toolInstances[context.toolInstances.length - 1];
}

let sourceNodeId: string;
let sourceHandle: string;

if (sourceToolNode) {
sourceNodeId = sourceToolNode.id;
sourceHandle = `${sourceToolNode.id}-tool-right-output-artifact`;
} else return; // Cannot create artifact node without a source tool

// Create artifact node positioned to the RIGHT of the tool node
const artifactNodeId = generateNodeId(manager, `Artifact_${artifactName}_${step.id}`);
const parentGroupId = subflow ? subflow.groupNode.id : undefined;

let artifactX: number;
let artifactY: number;

const ARTIFACT_X_OFFSET = 300; // Horizontal distance from tool to artifact
const ARTIFACT_DIFFERENCE_X = 100;

if (subflow) {
// For artifacts in a subflow, position relative to the group (like tools do)
const toolNode = nodes.find(n => n.id === sourceToolNode.id);
let relativeToolX: number;
let relativeToolY: number;

if (toolNode) {
// toolNode.position is already relative to the group
relativeToolX = toolNode.position.x;
relativeToolY = toolNode.position.y;
} else {
// Fallback: calculate relative position from absolute position
const groupX = subflow.groupNode.xPosition ?? LANE_X_POSITIONS.TOOLS;
relativeToolX = (sourceToolNode.xPosition ?? LANE_X_POSITIONS.TOOLS) - groupX;
relativeToolY = (sourceToolNode.yPosition ?? manager.nextAvailableGlobalY) - (subflow.groupNode.yPosition ?? manager.nextAvailableGlobalY);
}

// Position artifact relative to group, offset from the tool node
artifactX = relativeToolX + ARTIFACT_X_OFFSET;
artifactY = relativeToolY;
} else {
// For main flow, use absolute positioning (like tools do)
artifactX = (sourceToolNode.xPosition ?? LANE_X_POSITIONS.TOOLS) + ARTIFACT_X_OFFSET;
artifactY = sourceToolNode.yPosition ?? manager.nextAvailableGlobalY;
}

const artifactNode: Node = {
id: artifactNodeId,
type: "artifactNode",
position: { x: artifactX, y: artifactY },
data: {
label: "Artifact",
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The artifact node creation uses a generic "Artifact" label instead of including the actual artifact name. This makes it difficult to distinguish between multiple artifacts in the workflow diagram.

Consider updating the label to include the artifact name:

data: {
    label: artifactName,
    visualizerStepId: step.id,
},

This would make the diagram more informative and easier to understand.

Suggested change
label: "Artifact",
label: artifactName,

Copilot uses AI. Check for mistakes.
visualizerStepId: step.id,
},
parentId: parentGroupId,
};

addNode(nodes, manager.allCreatedNodeIds, artifactNode);
manager.nodePositions.set(artifactNodeId, { x: artifactX, y: artifactY });

const artifactInstance: NodeInstance = {
id: artifactNodeId,
xPosition: artifactX,
yPosition: artifactY,
height: NODE_HEIGHT,
width: NODE_WIDTH,
};

createTimelineEdge(sourceNodeId, artifactInstance.id, step, edges, manager, edgeAnimationService, processedSteps, sourceHandle, `${artifactInstance.id}-artifact-left-input`);

// Update maxY and maxContentXRelative to ensure group accommodates the artifact
const artifactBottom = artifactY + NODE_HEIGHT;
const artifactRight = artifactX + NODE_WIDTH;

if (subflow) {
subflow.maxY = Math.max(subflow.maxY, artifactBottom);

// Update maxContentXRelative to include artifact node
subflow.maxContentXRelative = Math.max(subflow.maxContentXRelative, artifactRight - ARTIFACT_DIFFERENCE_X);

// Update group dimensions
const requiredGroupWidth = subflow.maxContentXRelative + GROUP_PADDING_X;
subflow.groupNode.width = Math.max(subflow.groupNode.width || 0, requiredGroupWidth);

// Update the actual group node in the nodes array
const groupNodeData = nodes.find(n => n.id === subflow.groupNode.id);
if (groupNodeData && groupNodeData.style) {
groupNodeData.style.width = `${subflow.groupNode.width}px`;
}
}
currentPhase.maxY = Math.max(currentPhase.maxY, artifactBottom);
}

function handleAgentResponseText(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void {
const currentPhase = getCurrentPhase(manager);
// When step.isSubTaskStep is true, it indicates this is a response from Agent to Orchestrator (as a user)
Expand Down Expand Up @@ -702,6 +826,9 @@ export const transformProcessedStepsToTimelineFlow = (processedSteps: Visualizer
case "AGENT_RESPONSE_TEXT":
handleAgentResponseText(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps);
break;
case "AGENT_ARTIFACT_NOTIFICATION":
handleArtifactNotification(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps);
break;
case "TASK_COMPLETED":
handleTaskCompleted(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import GenericToolNode from "./FlowChart/customNodes/GenericToolNode";
import LLMNode from "./FlowChart/customNodes/LLMNode";
import OrchestratorAgentNode from "./FlowChart/customNodes/OrchestratorAgentNode";
import UserNode from "./FlowChart/customNodes/UserNode";
import ArtifactNode from "./FlowChart/customNodes/GenericArtifactNode";
import { VisualizerStepCard } from "./VisualizerStepCard";

const nodeTypes = {
Expand All @@ -26,6 +27,7 @@ const nodeTypes = {
llmNode: LLMNode,
orchestratorNode: OrchestratorAgentNode,
genericToolNode: GenericToolNode,
artifactNode: ArtifactNode,
};

const edgeTypes = {
Expand Down Expand Up @@ -262,6 +264,11 @@ const FlowRenderer: React.FC<FlowChartPanelProps> = ({ processedSteps, isRightPa
}
}

if (!targetEdge && node.type === "artifactNode") {
// For artifact nodes, find the tool that created it
targetEdge = edges.find(edge => edge.target === node.id) || null;
}

if (targetEdge) {
handleEdgeClick(_event, targetEdge);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from "react";

import { CheckCircle, FileText, HardDrive, Link, MessageSquare, Share2, Terminal, User, XCircle, Zap } from "lucide-react";
import { CheckCircle, FileText, HardDrive, Link, MessageSquare, Share2, Terminal, User, XCircle, Zap, ExternalLink } from "lucide-react";

import { JSONViewer, MarkdownHTMLConverter } from "@/lib/components";
import { useChatContext } from "@/lib/hooks";
import type { ArtifactNotificationData, LLMCallData, LLMResponseToAgentData, ToolDecisionData, ToolInvocationStartData, ToolResultData, VisualizerStep } from "@/lib/types";

interface VisualizerStepCardProps {
Expand All @@ -13,6 +14,8 @@ interface VisualizerStepCardProps {
}

const VisualizerStepCard: React.FC<VisualizerStepCardProps> = ({ step, isHighlighted, onClick, variant = "list" }) => {
const { artifacts, setPreviewArtifact, setActiveSidePanelTab, setIsSidePanelCollapsed } = useChatContext();

const getStepIcon = () => {
switch (step.type) {
case "USER_REQUEST":
Expand Down Expand Up @@ -156,24 +159,51 @@ const VisualizerStepCard: React.FC<VisualizerStepCardProps> = ({ step, isHighlig
</div>
</div>
);
const renderArtifactNotificationData = (data: ArtifactNotificationData) => (
<div className="mt-1.5 rounded-md bg-gray-50 p-2 text-xs text-gray-700 dark:bg-gray-700 dark:text-gray-300">
<p>
<strong>Artifact:</strong> {data.artifactName}
{data.version !== undefined && <span className="text-gray-500 dark:text-gray-400"> (v{data.version})</span>}
</p>
{data.mimeType && (
<p>
<strong>Type:</strong> {data.mimeType}
</p>
)}
{data.description && (
<p className="mt-1">
<strong>Description:</strong> {data.description}
</p>
)}
</div>
);
const renderArtifactNotificationData = (data: ArtifactNotificationData) => {
const handleViewFile = (e: React.MouseEvent) => {
e.stopPropagation();

// Find the artifact by filename
const artifact = artifacts.find(a => a.filename === data.artifactName);

if (artifact) {
// Switch to Files tab
setActiveSidePanelTab("files");

// Expand side panel if collapsed
setIsSidePanelCollapsed(false);

// Set preview artifact to open the file
setPreviewArtifact(artifact);
}
// If artifact not found, do nothing (silent failure)
};

return (
<div className="mt-1.5 rounded-md bg-gray-50 p-2 text-xs text-gray-700 dark:bg-gray-700 dark:text-gray-300">
<div className="flex items-center justify-between">
<p>
<strong>Artifact:</strong> {data.artifactName}
{data.version !== undefined && <span className="text-gray-500 dark:text-gray-400"> (v{data.version})</span>}
</p>
<button onClick={handleViewFile} className="flex items-center gap-1 text-blue-600 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" title="View in Files tab">
<span className="text-xs">View File</span>
<ExternalLink size={12} />
</button>
</div>
{data.mimeType && (
<p>
<strong>Type:</strong> {data.mimeType}
</p>
)}
{data.description && (
<p className="mt-1">
<strong>Description:</strong> {data.description}
</p>
)}
</div>
);
};

// Calculate indentation based on nesting level - only apply in list variant
const indentationStyle =
Expand Down
Loading
Loading