diff --git a/.gitignore b/.gitignore
index c65ba81..c762171 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ coverage/
.DS_Store
.vscode/
.idea/
-/temp/
\ No newline at end of file
+/temp/
+.gstack/
diff --git a/README.md b/README.md
index 423e865..ec93813 100644
--- a/README.md
+++ b/README.md
@@ -1,153 +1,275 @@
# @pipelex/mthds-ui
-Shared graph rendering logic for MTHDS method visualization. Core graph logic (builders, layout, analysis, configuration) is pure TypeScript with no React dependency. An optional React layer provides a ready-to-use `GraphViewer` component powered by ReactFlow.
+Interactive graph visualization for [MTHDS](https://mthds.sh) method pipelines. Renders execution graphs from [GraphSpec](https://docs.pipelex.com/latest/under-the-hood/execution-graph-tracing/#graphspec) data — the canonical format produced by [Pipelex](https://pipelex.com) when tracing pipeline execution.
+
+Core graph logic (builders, layout, analysis) is pure TypeScript with no React dependency. The React layer provides a drop-in `GraphViewer` component powered by [ReactFlow](https://reactflow.dev).
## Install
```bash
-npm install github:pipelex/mthds-ui
+npm install @pipelex/mthds-ui
```
### Peer dependencies
-| Dependency | Required | Used by |
-| -------------------- | ------------ | ----------------------------- |
-| `dagre` | **yes** | Graph layout engine |
-| `@types/dagre` | no (TS only) | Type definitions for dagre |
-| `react`, `react-dom` | no | React layer (`graph/react`) |
-| `@xyflow/react` | no | React layer (`graph/react`) |
-| `shiki` | no | Syntax highlighting (`shiki`) |
+| Dependency | Required | Used by |
+| -------------------- | ------------ | --------------------------- |
+| `dagre` | **yes** | Graph layout engine |
+| `@types/dagre` | no (TS only) | Type definitions for dagre |
+| `react`, `react-dom` | no | React layer (`graph/react`) |
+| `@xyflow/react` | no | React layer (`graph/react`) |
-## Entry points
+## Quick start (React)
-| Import path | Content |
-| ---------------------------------------------- | ---------------------------------------------------------------------- |
-| `@pipelex/mthds-ui` (or `./graph`) | Pure-TS graph logic — types, builders, layout, controllers, config |
-| `@pipelex/mthds-ui/graph/react` | React components — `GraphViewer`, `ControllerGroupNode`, label helpers |
-| `@pipelex/mthds-ui/graph/react/graph-core.css` | Base ReactFlow node/edge styles |
-| `@pipelex/mthds-ui/shiki` | MTHDS syntax highlighting with shiki |
+```tsx
+import { GraphViewer } from "@pipelex/mthds-ui/graph/react";
-## Quick start (pure TypeScript)
+function MethodGraph({ graphspec }) {
+ return ;
+}
+```
-```ts
-import {
- buildGraph,
- getLayoutedElements,
- applyControllers,
- DEFAULT_GRAPH_CONFIG,
-} from "@pipelex/mthds-ui";
+That's it. `GraphViewer` handles layout, styling, CSS variables, and all ReactFlow internals. All props except `graphspec` are optional with sensible defaults.
-// Build graph data from a GraphSpec
-const { graphData, analysis } = buildGraph(graphspec, "bezier");
+### Next.js (App Router)
-// Apply dagre layout
-const { nodes, edges } = getLayoutedElements(graphData.nodes, graphData.edges, "TB");
+ReactFlow accesses browser globals at module-evaluation time, so it cannot be server-side rendered. Use `next/dynamic` with `ssr: false`:
-// Optionally wrap nodes in controller groups
-const final = applyControllers(nodes, edges, graphspec, analysis, true);
+```tsx
+"use client";
+
+import dynamic from "next/dynamic";
+import type { GraphViewerProps } from "@pipelex/mthds-ui/graph/react";
+import React from "react";
+
+const GraphViewer = dynamic(
+ () =>
+ import("@pipelex/mthds-ui/graph/react").then((mod) => ({
+ default: mod.GraphViewer,
+ })),
+ { ssr: false },
+) as React.ComponentType;
+
+export function MyGraph({ graphspec }) {
+ return ;
+}
```
-## Quick start (React)
+### GraphViewer props
-```tsx
-import { ReactFlowProvider } from "@xyflow/react";
-import { DEFAULT_GRAPH_CONFIG } from "@pipelex/mthds-ui";
-import { GraphViewer } from "@pipelex/mthds-ui/graph/react";
-import "@xyflow/react/dist/style.css";
-import "@pipelex/mthds-ui/graph/react/graph-core.css";
+| Prop | Type | Default | Description |
+| ------------------- | ----------------------------------- | ---------------------------- | ------------------------------------------ |
+| `graphspec` | `GraphSpec \| null` | — | Graph data (nodes + edges) |
+| `config` | `GraphConfig` | `DEFAULT_GRAPH_CONFIG` | Layout and visual configuration |
+| `direction` | `GraphDirection` | `"TB"` | Layout direction: `TB`, `LR`, `RL`, `BT` |
+| `showControllers` | `boolean` | `false` | Show controller group outlines |
+| `onNavigateToPipe` | `(pipeCode: string) => void` | — | Callback when a pipe node is clicked |
+| `onReactFlowInit` | `(instance: AppRFInstance) => void` | — | Access the underlying ReactFlow instance |
-function MethodGraph({ graphspec }) {
- return (
-
- console.log("navigate:", pipeCode)}
- />
-
- );
-}
+### Container sizing
+
+The graph container uses `position: absolute; inset: 0` to fill its parent. Make sure the parent element has `position: relative` and a defined height (e.g. `h-full`, `flex-1`, or an explicit height).
+
+## GraphSpec
+
+GraphSpec is the data format that describes a pipeline execution graph. It's generated by Pipelex when running or validating MTHDS method bundles.
+
+### Generating a GraphSpec
+
+```bash
+# From a .mthds bundle file
+pipelex-agent validate bundle my-method.mthds --view
+
+# The output JSON contains a graphspec field
```
-### GraphViewer props
+Or programmatically via the Pipelex Python SDK:
+
+```python
+from pipelex import Pipelex
+
+px = Pipelex()
+result = px.validate_bundle("my-method.mthds", view=True)
+graphspec = result.graphspec # dict ready for JSON serialization
+```
+
+### Structure
+
+```typescript
+interface GraphSpec {
+ nodes: GraphSpecNode[];
+ edges: GraphSpecEdge[];
+}
+
+interface GraphSpecNode {
+ id: string;
+ pipe_code?: string; // e.g. "analyze_match"
+ pipe_type?: string; // e.g. "PipeLLM", "PipeSequence", "PipeExtract"
+ status?: string; // "SUCCEEDED", "FAILED", "RUNNING", etc.
+ io?: {
+ inputs?: IOItem[];
+ outputs?: IOItem[];
+ };
+}
-| Prop | Type | Description |
-| ------------------- | ----------------------------------- | ------------------------------------------- |
-| `graphspec` | `GraphSpec \| null` | Graph spec (nodes, edges, IO, containment) |
-| `config` | `GraphConfig` | Layout and visual configuration |
-| `direction` | `GraphDirection` | `"TB"`, `"LR"`, `"RL"`, or `"BT"` |
-| `showControllers` | `boolean` | Show controller group outlines |
-| `onNavigateToPipe?` | `(pipeCode: string) => void` | Callback when a pipe node is clicked |
-| `onReactFlowInit?` | `(instance: AppRFInstance) => void` | Access the ReactFlow instance |
-
-## CSS theming
-
-`graph-core.css` reads these CSS custom properties. Override them in your app to theme the graph:
-
-| Property | Purpose |
-| ------------------------- | -------------------------------- |
-| `--color-bg` | Graph background |
-| `--color-bg-dots` | Background dot pattern |
-| `--color-text-muted` | Edge label text |
-| `--color-accent` | Selected node highlight |
-| `--color-controller-text` | Controller group label/type text |
-| `--font-sans` | Node font family |
-| `--font-mono` | Controller label font family |
-| `--shadow-lg` | Selected node box shadow |
-
-## GraphConfig
-
-The `GraphConfig` object controls layout and visual behavior. Use `DEFAULT_GRAPH_CONFIG` as a starting point.
-
-```ts
-interface GraphConfig {
- direction?: "TB" | "LR" | "RL" | "BT"; // Dagre layout direction
- showControllers?: boolean; // Show controller group boxes
- nodesep?: number; // Horizontal spacing (default: 50)
- ranksep?: number; // Vertical spacing (default: 30)
- edgeType?: "bezier" | "step" | "straight" | "smoothstep";
- initialZoom?: number | null; // Override fit-view zoom (null = auto)
- panToTop?: boolean; // Pan viewport to top after layout
- paletteColors?: Record; // Color overrides for node/edge styles
+interface GraphSpecEdge {
+ id?: string;
+ source: string; // Source node ID
+ target: string; // Target node ID
+ kind: EdgeKind; // "data", "contains", "batch_item", etc.
+ label?: string;
}
```
-### Palette colors
+### Node types
+
+Nodes represent pipes (operations) and stuffs (data artifacts) in the pipeline:
+
+- **Pipe nodes** — operations like `PipeLLM`, `PipeSequence`, `PipeExtract`, `PipeSearch`
+- **Stuff nodes** — data flowing between pipes, typed by concepts (e.g. `CandidateProfile`, `Document`)
+
+### Edge kinds
+
+| Kind | Description |
+| -------------------- | -------------------------------------------------- |
+| `data` | Data flow — stuff produced by one pipe, consumed by another |
+| `contains` | Containment — a controller pipe wraps child pipes |
+| `batch_item` | Batch processing — items fanned out from a collection |
+| `batch_aggregate` | Batch aggregation — items collected back |
+| `parallel_combine` | Parallel results combined |
+
+### Example GraphSpec (minimal)
+
+```json
+{
+ "nodes": [
+ { "id": "input_doc", "pipe_type": "Input" },
+ { "id": "extract_text", "pipe_code": "extract_text", "pipe_type": "PipeExtract" },
+ { "id": "pages", "pipe_type": "Stuff" },
+ { "id": "summarize", "pipe_code": "summarize", "pipe_type": "PipeLLM" },
+ { "id": "summary", "pipe_type": "Stuff" }
+ ],
+ "edges": [
+ { "source": "input_doc", "target": "extract_text", "kind": "data" },
+ { "source": "extract_text", "target": "pages", "kind": "data" },
+ { "source": "pages", "target": "summarize", "kind": "data" },
+ { "source": "summarize", "target": "summary", "kind": "data" }
+ ]
+}
+```
-`paletteColors` controls the colors used when building nodes and edges. Keys are CSS custom property names (e.g. `--color-pipe`, `--color-stuff`, `--color-edge`). See `graphConfig.ts` for the full default palette.
+Full specification: [docs.pipelex.com — Execution Graph Tracing](https://docs.pipelex.com/latest/under-the-hood/execution-graph-tracing/#graphspec)
-## Type boundary: domain types vs ReactFlow types
+## Configuration
-The graph module separates two type worlds:
+### GraphConfig
-- **Domain types** (`graph/types.ts`): `GraphNode`, `GraphEdge`, `GraphNodeData` — used by all pure graph logic (builders, layout, controllers, analysis). No React dependency.
-- **ReactFlow types** (`graph/react/rfTypes.ts`): `AppNode`, `AppEdge`, `AppRFInstance` — ReactFlow generics parameterized with domain data. Used only in the React layer.
+Controls layout and visual behavior. All fields are optional — `DEFAULT_GRAPH_CONFIG` provides sensible defaults.
-If you bypass `GraphViewer` and drive ReactFlow directly, convert at the boundary:
+```typescript
+import { DEFAULT_GRAPH_CONFIG } from "@pipelex/mthds-ui";
-```ts
-import { toAppNodes, toAppEdges } from "@pipelex/mthds-ui/graph/react";
+// Override specific settings
+const myConfig = {
+ ...DEFAULT_GRAPH_CONFIG,
+ direction: "LR",
+ nodesep: 80,
+ ranksep: 50,
+};
+```
-setNodes(toAppNodes(domainNodes));
-setEdges(toAppEdges(domainEdges));
+| Field | Type | Default | Description |
+| ------------------ | --------------------- | ---------- | ---------------------------------------- |
+| `direction` | `GraphDirection` | `"TB"` | Dagre layout direction |
+| `showControllers` | `boolean` | `false` | Show controller group boxes |
+| `nodesep` | `number` | `50` | Horizontal spacing between nodes |
+| `ranksep` | `number` | `30` | Vertical spacing between ranks |
+| `edgeType` | `EdgeType` | `"bezier"` | Edge curve style |
+| `initialZoom` | `number \| null` | `null` | Override fit-view zoom (`null` = auto) |
+| `panToTop` | `boolean` | `true` | Pan viewport to top after layout |
+| `paletteColors` | `Record` | *(see below)* | CSS variable overrides for theming |
+
+### Theming with palette colors
+
+`GraphViewer` applies CSS custom properties from `paletteColors` on mount. Override colors by passing a custom config:
+
+```tsx
+
```
-Never use `as any` to pass `GraphNode[]` to `setNodes` — use the mapping functions.
+Default palette colors include:
+
+| Variable | Purpose |
+| --------------------------- | -------------------------- |
+| `--color-pipe` | Pipe node border/accent |
+| `--color-pipe-bg` | Pipe node background |
+| `--color-stuff` | Stuff node border/accent |
+| `--color-stuff-bg` | Stuff node background |
+| `--color-edge` | Edge line color |
+| `--color-batch-item` | Batch item edge color |
+| `--color-batch-aggregate` | Batch aggregate edge color |
+| `--color-bg` | Graph background |
+| `--color-bg-dots` | Background dot pattern |
+| `--font-sans` | Node font family |
+| `--font-mono` | Code/controller font |
+
+See `graphConfig.ts` for the full default palette.
+
+## Entry points
+
+| Import path | Content |
+| ------------------------------- | ------------------------------------------------------------------ |
+| `@pipelex/mthds-ui` | Pure-TS graph logic — types, builders, layout, controllers, config |
+| `@pipelex/mthds-ui/graph/react` | React components — `GraphViewer`, label helpers, type converters |
+| `@pipelex/mthds-ui/shiki` | MTHDS syntax highlighting with shiki |
+
+## Pure TypeScript usage
+
+Use the graph logic without React — build nodes/edges, run layout, and feed the result to your own renderer:
+
+```typescript
+import {
+ buildGraph,
+ getLayoutedElements,
+ applyControllers,
+ DEFAULT_GRAPH_CONFIG,
+} from "@pipelex/mthds-ui";
+
+// Build graph data from a GraphSpec
+const { graphData, analysis } = buildGraph(graphspec, "bezier");
+
+// Apply dagre layout
+const { nodes, edges } = getLayoutedElements(
+ graphData.nodes,
+ graphData.edges,
+ "TB",
+);
+
+// Optionally wrap nodes in controller groups
+const final = applyControllers(nodes, edges, graphspec, analysis, true);
+```
## Shiki integration
Syntax-highlight MTHDS code with the bundled grammar and themes:
-```ts
+```typescript
import { highlightMthds, getAvailableThemes } from "@pipelex/mthds-ui/shiki";
const html = await highlightMthds(mthdsSource, "pipelex-dark");
```
-For custom shiki setups, use `getMthdsGrammar()` and `getMthdsTheme()` to register the MTHDS language and theme with your own highlighter instance.
-
## Development
```bash
diff --git a/package-lock.json b/package-lock.json
index cba5385..f7da54f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@pipelex/mthds-ui",
- "version": "0.1.3",
+ "version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pipelex/mthds-ui",
- "version": "0.1.3",
+ "version": "0.1.4",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.39.4",
diff --git a/src/graph/graphConfig.ts b/src/graph/graphConfig.ts
index 2c27034..a110bd4 100644
--- a/src/graph/graphConfig.ts
+++ b/src/graph/graphConfig.ts
@@ -1,7 +1,7 @@
import type { GraphConfig } from "./types";
export const DEFAULT_GRAPH_CONFIG: GraphConfig = {
- direction: "TB",
+ direction: "LR",
showControllers: false,
nodesep: 50,
ranksep: 30,
@@ -9,6 +9,7 @@ export const DEFAULT_GRAPH_CONFIG: GraphConfig = {
initialZoom: null,
panToTop: true,
paletteColors: {
+ // Graph node/edge colors
"--color-pipe": "#ff6b6b",
"--color-pipe-bg": "rgba(224,108,117,0.18)",
"--color-pipe-text": "#ffffff",
@@ -27,6 +28,14 @@ export const DEFAULT_GRAPH_CONFIG: GraphConfig = {
"--color-error-bg": "rgba(255,85,85,0.15)",
"--color-accent": "#8BE9FD",
"--color-warning": "#FFB86C",
+ // Base theme vars used by graph-core.css
+ "--color-bg": "#1e1e1e",
+ "--color-bg-dots": "#334155",
+ "--color-text-muted": "#94a3b8",
+ "--color-controller-text": "#94a3b8",
+ "--font-sans": '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
+ "--font-mono": '"JetBrains Mono", "Monaco", "Menlo", monospace',
+ "--shadow-lg": "0 8px 24px rgba(0, 0, 0, 0.5)",
},
};
diff --git a/src/graph/react/GraphViewer.tsx b/src/graph/react/GraphViewer.tsx
index 06cee87..b62c3af 100644
--- a/src/graph/react/GraphViewer.tsx
+++ b/src/graph/react/GraphViewer.tsx
@@ -20,14 +20,15 @@ import { toAppNodes, toAppEdges } from "./rfTypes";
import { buildGraph } from "../graphBuilders";
import { getLayoutedElements, ensureControllerSpacing } from "../graphLayout";
import { applyControllers } from "../graphControllers";
+import { DEFAULT_GRAPH_CONFIG } from "../graphConfig";
import { hydrateLabels } from "./renderLabel";
import { controllerNodeTypes } from "./ControllerGroupNode";
export interface GraphViewerProps {
graphspec: GraphSpec | null;
- config: GraphConfig;
- direction: GraphDirection;
- showControllers: boolean;
+ config?: GraphConfig;
+ direction?: GraphDirection;
+ showControllers?: boolean;
onNavigateToPipe?: (pipeCode: string) => void;
onReactFlowInit?: (instance: AppRFInstance) => void;
}
@@ -42,8 +43,24 @@ function cloneCachedNodes(nodes: GraphNode[]): GraphNode[] {
}
export function GraphViewer(props: GraphViewerProps) {
- const { graphspec, config, direction, showControllers, onNavigateToPipe, onReactFlowInit } =
- props;
+ const {
+ graphspec,
+ config = DEFAULT_GRAPH_CONFIG,
+ direction = config.direction ?? DEFAULT_GRAPH_CONFIG.direction ?? "TB",
+ showControllers = config.showControllers ?? DEFAULT_GRAPH_CONFIG.showControllers ?? false,
+ onNavigateToPipe,
+ onReactFlowInit,
+ } = props;
+
+ // Apply palette CSS vars on mount (so consumers don't have to)
+ React.useEffect(() => {
+ const palette = config.paletteColors ?? DEFAULT_GRAPH_CONFIG.paletteColors;
+ if (palette) {
+ for (const [cssVar, value] of Object.entries(palette)) {
+ document.documentElement.style.setProperty(cssVar, value);
+ }
+ }
+ }, [config.paletteColors]);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
diff --git a/src/graph/react/graph-core.css b/src/graph/react/graph-core.css
index f9a8d58..cf23a78 100644
--- a/src/graph/react/graph-core.css
+++ b/src/graph/react/graph-core.css
@@ -1,6 +1,6 @@
.react-flow-container {
- width: 100%;
- height: 100%;
+ position: absolute;
+ inset: 0;
background: var(--color-bg);
}
.react-flow__node {
diff --git a/src/graph/react/index.ts b/src/graph/react/index.ts
index 8de9b25..71aaecd 100644
--- a/src/graph/react/index.ts
+++ b/src/graph/react/index.ts
@@ -1,3 +1,8 @@
+"use client";
+
+import "@xyflow/react/dist/style.css";
+import "./graph-core.css";
+
export { GraphViewer } from "./GraphViewer";
export type { GraphViewerProps } from "./GraphViewer";
export type { AppNode, AppEdge, AppRFInstance } from "./rfTypes";
diff --git a/tsup.config.ts b/tsup.config.ts
index 4ed9898..206b250 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -15,7 +15,10 @@ export default defineConfig({
"react",
"react-dom",
"@xyflow/react",
+ /graph-core\.css$/,
],
+ // Keep graph-core.css as a standalone export for consumers that need it separately.
+ // GraphViewer.tsx also imports it directly, so it gets bundled into the React entry's CSS.
onSuccess: async () => {
mkdirSync("dist/graph/react", { recursive: true });
cpSync("src/graph/react/graph-core.css", "dist/graph/react/graph-core.css");