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
434 changes: 234 additions & 200 deletions ui/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.10.2",
"esbuild": "0.27.3",
"lucide-react": "^0.577.0",
"react": "^19.1.1",
Expand Down
65 changes: 65 additions & 0 deletions ui/src/components/workflows/canvas/WorkflowCanvas.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* WorkflowCanvas tests.
*
* Verifies that the canvas renders without errors and that the React Flow
* sub-components (controls, minimap, background) are present in the DOM.
*/

import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { WorkflowCanvas } from "./WorkflowCanvas";

// React Flow uses ResizeObserver internally; provide a stub in jsdom.
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};

describe("WorkflowCanvas", () => {
const defaultProps = {
nodes: [],
edges: [],
onNodesChange: () => {},
onEdgesChange: () => {},
onConnect: () => {},
};

it("renders without throwing", () => {
expect(() => render(<WorkflowCanvas {...defaultProps} />)).not.toThrow();
});

it("renders the canvas wrapper", () => {
render(<WorkflowCanvas {...defaultProps} />);
expect(screen.getByTestId("workflow-canvas")).toBeInTheDocument();
});

it("accepts custom className", () => {
render(
<WorkflowCanvas {...defaultProps} className="my-custom-class" />,
);
const wrapper = screen.getByTestId("workflow-canvas");
expect(wrapper.className).toContain("my-custom-class");
});

it("renders with nodes and edges without throwing", () => {
const nodes = [
{
id: "n1",
type: "default",
position: { x: 0, y: 0 },
data: { label: "Node 1" },
},
];
const edges = [{ id: "e1", source: "n1", target: "n2" }];
expect(() =>
render(
<WorkflowCanvas
{...defaultProps}
nodes={nodes}
edges={edges}
/>,
),
).not.toThrow();
});
});
130 changes: 130 additions & 0 deletions ui/src/components/workflows/canvas/WorkflowCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* WorkflowCanvas — base React Flow canvas for the visual workflow builder.
*
* Renders a ReactFlow instance with Controls, MiniMap, and Background.
* Accepts standard React Flow props so parent components can manage
* node/edge state and handle connection events.
*
* Wrap a page-level ancestor in <ReactFlowProvider> when multiple canvas
* instances need to coexist; for single-canvas pages the provider is
* included here.
*/

import "@xyflow/react/dist/style.css";
import {
Background,
BackgroundVariant,
Controls,
MiniMap,
ReactFlow,
ReactFlowProvider,
type Edge,
type EdgeTypes,
type Node,
type NodeTypes,
type OnConnect,
type OnEdgesChange,
type OnNodesChange,
} from "@xyflow/react";

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------

export interface WorkflowCanvasProps {
/** Current node list */
nodes: Node[];
/** Current edge list */
edges: Edge[];
/** Handler for node changes (move, select, remove) */
onNodesChange: OnNodesChange;
/** Handler for edge changes (select, remove) */
onEdgesChange: OnEdgesChange;
/** Handler for new connections drawn by the user */
onConnect: OnConnect;
/** Custom node type registry — merge with workflowNodeTypes */
nodeTypes?: NodeTypes;
/** Custom edge type registry — merge with workflowEdgeTypes */
edgeTypes?: EdgeTypes;
/** Optional CSS class applied to the outer wrapper */
className?: string;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

function Canvas({
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
nodeTypes,
edgeTypes,
className = "",
}: WorkflowCanvasProps) {
return (
<div
className={`h-full w-full ${className}`}
data-testid="workflow-canvas"
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
snapToGrid
snapGrid={[16, 16]}
attributionPosition="bottom-right"
style={{
// Map agentd theme tokens to React Flow CSS variables so the
// canvas inherits the active colour scheme automatically.
// biome-ignore lint/suspicious/noExplicitAny: CSS custom property assignment
["--xy-background-color" as any]: "var(--th-surface)",
["--xy-node-border-color" as any]: "var(--th-border)",
["--xy-edge-stroke" as any]: "var(--th-text-muted)",
["--xy-edge-stroke-selected" as any]: "var(--th-accent)",
["--xy-minimap-background-color" as any]: "var(--th-surface-sunken)",
["--xy-controls-button-background-color" as any]: "var(--th-surface)",
["--xy-controls-button-border-color" as any]: "var(--th-border)",
["--xy-controls-button-color" as any]: "var(--th-text-secondary)",
["--xy-controls-button-background-color-hover" as any]:
"var(--th-surface-hover)",
}}
>
<Controls />
<MiniMap
nodeColor={() => "var(--th-accent)"}
maskColor="rgba(0,0,0,0.12)"
/>
<Background
variant={BackgroundVariant.Dots}
gap={16}
size={1}
color="var(--th-border)"
/>
</ReactFlow>
</div>
);
}

/**
* WorkflowCanvas wraps the inner canvas in a ReactFlowProvider so it can be
* dropped into any page without requiring a provider higher in the tree.
* If you need multiple canvases on one page, render each inside its own
* WorkflowCanvas (each gets its own provider context).
*/
export function WorkflowCanvas(props: WorkflowCanvasProps) {
return (
<ReactFlowProvider>
<Canvas {...props} />
</ReactFlowProvider>
);
}

export default WorkflowCanvas;
2 changes: 2 additions & 0 deletions ui/src/components/workflows/canvas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { WorkflowCanvasProps } from "./WorkflowCanvas";
export { WorkflowCanvas } from "./WorkflowCanvas";
2 changes: 2 additions & 0 deletions ui/src/components/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type { WorkflowCanvasProps } from "./canvas";
export { WorkflowCanvas } from "./canvas";
export type { DispatchHistoryProps } from "./DispatchHistory";
export { DispatchHistory } from "./DispatchHistory";
export type { PromptTemplateEditorProps } from "./PromptTemplateEditor";
Expand Down
Loading