diff --git a/src/renderer/components/PatternEditor.tsx b/src/renderer/components/PatternEditor.tsx index 1c7380c..57cec9a 100644 --- a/src/renderer/components/PatternEditor.tsx +++ b/src/renderer/components/PatternEditor.tsx @@ -318,6 +318,7 @@ export function PatternEditor({
= { 'user-input': CircleUser, 'user-output': CircleUser, - agent: Bot, + agent: Bot, // fallback when no provider is resolved distributor: Shuffle, collector: Layers, orchestrator: Radio, @@ -24,23 +25,30 @@ const kindColors: Record { + if (isAgent && data.provider) { + return ; + } + const FallbackIcon = kindIcons[data.kind] ?? Bot; + return ; + }; + return (
- + {renderIcon()}
{data.label}
- {isAgent && typeof data.order === 'number' && ( -
#{data.order + 1}
+ {isAgent && data.modelLabel && ( +
{data.modelLabel}
)}
{data.readOnly && ( diff --git a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx index 3ffcb55..9197ee0 100644 --- a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx +++ b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx @@ -3,6 +3,7 @@ import { ReactFlow, Background, BackgroundVariant, + MarkerType, useNodesState, useEdgesState, type Node, @@ -15,6 +16,7 @@ import '@xyflow/react/dist/style.css'; import type { OrchestrationMode, PatternDefinition, PatternGraph } from '@shared/domain/pattern'; import { resolvePatternGraph } from '@shared/domain/pattern'; +import type { ModelDefinition } from '@shared/domain/models'; import { addHandoffEdge, fromCanvasPositions, @@ -30,6 +32,7 @@ import { graphNodeTypes } from './GraphNodes'; interface PatternGraphCanvasProps { pattern: PatternDefinition; + availableModels?: ReadonlyArray; onGraphChange: (graph: PatternGraph) => void; onNodeSelect: (nodeId: string | null) => void; selectedNodeId: string | null; @@ -37,6 +40,7 @@ interface PatternGraphCanvasProps { export function PatternGraphCanvas({ pattern, + availableModels, onGraphChange, onNodeSelect, selectedNodeId, @@ -45,7 +49,7 @@ export function PatternGraphCanvas({ const draggingRef = useRef(false); const [nodes, setNodes, onNodesChange] = useNodesState( - toCanvasNodes(graph, pattern.agents), + toCanvasNodes(graph, pattern.agents, availableModels), ); const [edges, setEdges, onEdgesChange] = useEdgesState( toCanvasEdges(graph, pattern.mode), @@ -53,9 +57,9 @@ export function PatternGraphCanvas({ // Sync canvas when pattern changes externally useEffect(() => { - setNodes(toCanvasNodes(graph, pattern.agents)); + setNodes(toCanvasNodes(graph, pattern.agents, availableModels)); setEdges(toCanvasEdges(graph, pattern.mode)); - }, [graph, pattern.agents, pattern.mode, setNodes, setEdges]); + }, [graph, pattern.agents, pattern.mode, availableModels, setNodes, setEdges]); const handleNodesChange: OnNodesChange> = useCallback( (changes) => { @@ -162,6 +166,7 @@ export function PatternGraphCanvas({ defaultEdgeOptions={{ type: 'smoothstep', style: { stroke: '#52525b', strokeWidth: 1.5 }, + markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16, color: '#52525b' }, }} connectionLineStyle={{ stroke: '#6366f1', strokeWidth: 1.5 }} deleteKeyCode="Delete" diff --git a/src/renderer/lib/patternGraph.ts b/src/renderer/lib/patternGraph.ts index 33174c0..802eedb 100644 --- a/src/renderer/lib/patternGraph.ts +++ b/src/renderer/lib/patternGraph.ts @@ -1,4 +1,4 @@ -import type { Node, Edge, Connection } from '@xyflow/react'; +import { MarkerType, type Node, type Edge, type Connection } from '@xyflow/react'; import type { OrchestrationMode, @@ -10,6 +10,8 @@ import type { PatternGraphNodeKind, } from '@shared/domain/pattern'; import { resolvePatternGraph } from '@shared/domain/pattern'; +import type { ModelProvider } from '@shared/domain/models'; +import { inferProvider, findModel, type ModelDefinition } from '@shared/domain/models'; /* ── Canvas node data ──────────────────────────────────────── */ @@ -19,6 +21,10 @@ export interface GraphNodeData extends Record { agentId?: string; order?: number; readOnly: boolean; + /** AI provider inferred from the agent's model (agent nodes only). */ + provider?: ModelProvider; + /** Short display name for the agent's model (agent nodes only). */ + modelLabel?: string; } /* ── View-model projection ─────────────────────────────────── */ @@ -57,22 +63,42 @@ function resolveNodeType(kind: PatternGraphNodeKind): string { } } -export function toCanvasNodes(graph: PatternGraph, agents: PatternAgentDefinition[]): Node[] { - return graph.nodes.map((node) => ({ - id: node.id, - type: resolveNodeType(node.kind), - position: { x: node.position.x, y: node.position.y }, - data: { - label: resolveNodeLabel(node, agents), - kind: node.kind, - agentId: node.agentId, - order: node.order, - readOnly: isSystemNode(node.kind), - }, - draggable: true, - selectable: true, - deletable: false, - })); +export function toCanvasNodes( + graph: PatternGraph, + agents: PatternAgentDefinition[], + models?: ReadonlyArray, +): Node[] { + return graph.nodes.map((node) => { + let provider: ModelProvider | undefined; + let modelLabel: string | undefined; + + if (node.kind === 'agent' && node.agentId) { + const agent = agents.find((a) => a.id === node.agentId); + if (agent?.model) { + provider = inferProvider(agent.model); + const modelDef = models ? findModel(agent.model, models) : undefined; + modelLabel = modelDef?.name ?? agent.model; + } + } + + return { + id: node.id, + type: resolveNodeType(node.kind), + position: { x: node.position.x, y: node.position.y }, + data: { + label: resolveNodeLabel(node, agents), + kind: node.kind, + agentId: node.agentId, + order: node.order, + readOnly: isSystemNode(node.kind), + provider, + modelLabel, + }, + draggable: true, + selectable: true, + deletable: false, + }; + }); } /** Determines whether user-created edges can be deleted in this mode. */ @@ -97,6 +123,8 @@ export function toCanvasEdges(graph: PatternGraph, mode: OrchestrationMode): Edg type: 'smoothstep', animated: mode === 'handoff', deletable: isEdgeDeletable(edge, mode, graph), + markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16, color: '#52525b' }, + style: { stroke: '#52525b', strokeWidth: 1.5 }, })); } diff --git a/tests/renderer/patternGraph.test.ts b/tests/renderer/patternGraph.test.ts index 4643871..b170563 100644 --- a/tests/renderer/patternGraph.test.ts +++ b/tests/renderer/patternGraph.test.ts @@ -258,4 +258,46 @@ describe('edge deletion rules', () => { expect(inputNode!.type).toBe('userInputNode'); expect(outputNode!.type).toBe('userOutputNode'); }); + + test('edges have directional arrow markers', () => { + const pattern = findPattern('sequential'); + const graph = resolvePatternGraph(pattern); + const edges = toCanvasEdges(graph, pattern.mode); + + expect(edges.length).toBeGreaterThan(0); + for (const edge of edges) { + expect(edge.markerEnd).toBeDefined(); + expect((edge.markerEnd as { type: string }).type).toBe('arrowclosed'); + } + }); + + test('agent nodes include provider and model label when models catalog is provided', () => { + const { modelCatalog } = require('@shared/domain/models'); + const pattern = findPattern('sequential'); + const graph = resolvePatternGraph(pattern); + const nodes = toCanvasNodes(graph, pattern.agents, modelCatalog); + + const agentNodes = nodes.filter((n) => n.data.kind === 'agent'); + expect(agentNodes.length).toBeGreaterThan(0); + + for (const node of agentNodes) { + expect(node.data.provider).toBeDefined(); + expect(node.data.modelLabel).toBeDefined(); + expect(node.data.modelLabel!.length).toBeGreaterThan(0); + } + }); + + test('agent nodes infer provider from model id without models catalog', () => { + const pattern = findPattern('sequential'); + const graph = resolvePatternGraph(pattern); + const nodes = toCanvasNodes(graph, pattern.agents); + + const agentNodes = nodes.filter((n) => n.data.kind === 'agent'); + for (const node of agentNodes) { + // Provider should still be inferred from model id prefix + expect(node.data.provider).toBeDefined(); + // Without catalog, modelLabel falls back to the raw model id + expect(node.data.modelLabel).toBeDefined(); + } + }); });