-
+ {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();
+ }
+ });
});