Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ jobs:
node-version: "20"
cache: "npm"
- run: npm install
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm test
- run: npm run test:react:release
- run: |
echo "validate examples"
for f in examples/*.json; do
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ jobs:
- if: steps.release.outputs.should_release == 'true'
run: npm ci
- if: steps.release.outputs.should_release == 'true'
run: npm run build
run: npx playwright install --with-deps chromium
- if: steps.release.outputs.should_release == 'true'
run: npm test
run: npm run release:wire-react:dry-run

- name: Pack workspace packages
if: steps.release.outputs.should_release == 'true'
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
```
packages/
wire-core — schema, validation, normalization, layout
wire-renderers — React Flow / xyflow adapter
wire-renderers — static renderers and graph-canvas compatibility adapter
wire-react — JSX facade
wire-mcp — MCP server (stdio + HTTP)
wire-cli — CLI binary
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ LLMs and agents struggle with diagrams: they emit Mermaid blobs that almost rend
- **Shared actions** — every human, hosted editor, CLI, and MCP edit flows through the `WireAction` reducer in `wire-core`.
- **React library** — `WireEditor`, `WireViewer`, `WireCanvas`, palette, toolbar, inspector, validation panel, and JSX authoring.
- **LLM-friendly React extensibility** — option catalogs, custom node cards, custom edge renderers, and custom group renderers without requiring app code to import a graph-canvas package.
- **Static renderers** — SVG, PNG, Mermaid, and optional React Flow conversion without pulling React into server-only consumers.
- **Static renderers** — SVG, PNG, Mermaid, and optional canvas-adapter conversion without pulling React into server-only consumers.
- **MCP server** — diagram CRUD, direct action tools, atomic `apply_actions`, resources, prompts, render tools, and `v1_get_agent_guide` over stdio or HTTP.
- **Hosted parity** — the playground/editor uses `@aigentive/wire-react`, stores canonical JSON, and renders from the same model as MCP.

Expand All @@ -21,7 +21,7 @@ LLMs and agents struggle with diagrams: they emit Mermaid blobs that almost rend
| Package | Description |
|---|---|
| [`@aigentive/wire-core`](packages/wire-core) | Schema, validation, IDs, graph normalization, layout, pure `WireAction` reducer |
| [`@aigentive/wire-renderers`](packages/wire-renderers) | Static renderers and adapters: SVG, PNG helpers, Mermaid, optional React Flow conversion |
| [`@aigentive/wire-renderers`](packages/wire-renderers) | Static renderers and adapters: SVG, PNG helpers, Mermaid, optional canvas-adapter conversion |
| [`@aigentive/wire-react`](packages/wire-react) | Reusable React editor/viewer components and JSX authoring facade |
| [`@aigentive/wire-mcp`](packages/wire-mcp) | MCP server (stdio + streamable-HTTP) |
| [`@aigentive/wire-cli`](packages/wire-cli) | `wire` CLI (init, add, validate, export, ls) |
Expand Down Expand Up @@ -82,6 +82,7 @@ Controlled editor:
```tsx
import type { WireDiagram } from "@aigentive/wire-core";
import { WireEditor, WireViewer } from "@aigentive/wire-react";
import "@aigentive/wire-react/styles.css";

export function WorkflowEditor({
diagram,
Expand All @@ -101,6 +102,7 @@ export function WorkflowPreview({ diagram }: { diagram: WireDiagram }) {
Custom editor surface:

```tsx
import "@aigentive/wire-react/styles.css";
import {
WireCanvas,
WireInspector,
Expand All @@ -126,6 +128,7 @@ export function ProductEditor({ diagram, onChange }) {
LLM-friendly custom cards and options:

```tsx
import "@aigentive/wire-react/styles.css";
import {
WireWorkspace,
type WireOptionCatalog
Expand Down Expand Up @@ -154,6 +157,8 @@ export function AgentEditor({ diagram, onChange }) {
`WireWorkspace` uses a decoupled event model: card/list clicks emit
`node.inspect` and update selection; option panels can follow selection by
default or receive an explicit `inspectNodeId` for fully controlled sidebars.
The package stylesheet is the supported npm-consumer styling path; no utility
source scan is required for the React package.

See [`docs/REACT_COMPONENTS.md`](docs/REACT_COMPONENTS.md), the playground
route `/docs`, and the playground route `/samples/agent-chain`.
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ npx -y @aigentive/wire-mcp@latest --http`;
const PACKAGES = [
{ name: "@aigentive/wire-core", desc: "Schema, validation, IDs, normalize, layout, the WireAction reducer." },
{ name: "@aigentive/wire-react", desc: "Editor, viewer, canvas, palette, inspector, hooks, JSX facade." },
{ name: "@aigentive/wire-renderers", desc: "Static SVG, PNG helpers, Mermaid, optional React Flow conversion." },
{ name: "@aigentive/wire-renderers", desc: "Static SVG, PNG helpers, Mermaid, optional canvas-adapter conversion." },
{ name: "@aigentive/wire-mcp", desc: "MCP server over stdio + streamable-HTTP with the full tool surface." },
{ name: "@aigentive/wire-cli", desc: "wire init · add · validate · export · ls." },
{ name: "apps/playground", desc: "Self-hostable Next.js editor, share API, and renderer service." }
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/app/api/playground/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ SCOPE AND TRUST BOUNDARIES:
Expected outcome:
- Produce one complete canonical WireDiagram JSON object.
- Preserve useful existing node ids, positions, sizes, labels, and manual edits unless the user asks to replace them.
- Use only Wire schema fields. Never emit React Flow JSON, SVG, Markdown diagrams, or prose as the primary artifact.
- Use only Wire schema fields. Never emit adapter JSON, SVG, Markdown diagrams, or prose as the primary artifact.
- Valid tones are only default, success, warning, error, info, ai. Never use danger.
- Prefer clear workflow-wireframe diagrams with 4-8 nodes unless the user asks for more detail.
- Every non-group workflow node renders as a card. Use node.kind for the card category, node.title for the card header, and node.description for body copy.
Expand Down
9 changes: 9 additions & 0 deletions apps/playground/app/docs/_components/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export const NAV: NavSection[] = [
{
title: "Examples",
items: [
{ href: "/docs/examples/package-css", label: "Package CSS", badge: "new" },
{ href: "/docs/examples/custom-shell", label: "Custom shell", badge: "new" },
{ href: "/docs/examples/options", label: "Options", badge: "new" },
{ href: "/docs/examples/controlled-state", label: "Controlled state", badge: "new" },
{ href: "/docs/examples/edge-inspection", label: "Edge inspection", badge: "new" },
{ href: "/docs/examples/accessibility", label: "Accessibility", badge: "new" },
{ href: "/docs/examples/theming", label: "Theming", badge: "new" },
{ href: "/docs/examples/wrappers", label: "Wrappers", badge: "new" },
{ href: "/docs/examples/read-only-inspector", label: "Read-only inspector", badge: "new" },
{ href: "/docs/examples/layouts", label: "Layouts" },
{ href: "/docs/examples/click-modal", label: "Click → modal" },
{ href: "/docs/examples/click-sidebar", label: "Click → sidebar" }
Expand Down
86 changes: 86 additions & 0 deletions apps/playground/app/docs/examples/accessibility/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";

import { WireCanvas, WireProvider } from "@aigentive/wire-react";
import { DocsPage } from "../../_components/DocsPage";
import { CodePreview } from "../../_components/CodePreview";
import { Prose, InlineCode } from "../../_components/Prose";
import { ExampleSurface, PRODUCTION_DIAGRAM, PRODUCTION_OPTIONS } from "../production-shared";

const ACCESSIBILITY_SNIPPET = `import "@aigentive/wire-react/styles.css";
import { WireCanvas, WireProvider } from "@aigentive/wire-react";

export function AccessibleCanvas({ diagram, optionCatalog }) {
return (
<WireProvider defaultDiagram={diagram} validateOnChange>
<WireCanvas
mode="edit"
fitView
keyboardA11y
nodesFocusable
edgesFocusable
autoPanOnNodeFocus
optionCatalog={optionCatalog}
ariaLabelConfig={{
canvas: "Agent workflow canvas",
node: (node) => \`Workflow node: \${node.title}\`,
edge: (edge) => \`Workflow edge: \${edge.label ?? edge.id}\`,
search: "Search workflow items",
connectionTarget: "Choose workflow connection target"
}}
isValidConnection={({ sourceNode, targetNode }) =>
sourceNode.id === targetNode.id ? "Choose a different target." : true
}
/>
</WireProvider>
);
}`;

export default function AccessibilityExamplePage() {
return (
<DocsPage
eyebrow="Examples"
title="Accessibility"
description="Use package-owned keyboard behavior, ARIA labels, search, and connection validation."
crumbs={[{ href: "/", label: "Docs" }, { label: "Examples" }, { label: "Accessibility" }]}
next={{ href: "/examples/theming", label: "Theming" }}
>
<Prose>
<h2 id="canvas-a11y">Canvas accessibility</h2>
<p>
<InlineCode>keyboardA11y</InlineCode> is on by default. Override labels with{" "}
<InlineCode>ariaLabelConfig</InlineCode>, and return a string from <InlineCode>isValidConnection</InlineCode>
to announce connection rejection.
</p>
</Prose>
<CodePreview
snippet={ACCESSIBILITY_SNIPPET}
height={480}
preview={
<ExampleSurface height={480}>
<WireProvider defaultDiagram={PRODUCTION_DIAGRAM} validateOnChange>
<WireCanvas
mode="edit"
fitView
keyboardA11y
nodesFocusable
edgesFocusable
autoPanOnNodeFocus
optionCatalog={PRODUCTION_OPTIONS}
ariaLabelConfig={{
canvas: "Agent workflow canvas",
node: (node) => `Workflow node: ${node.title}`,
edge: (edge) => `Workflow edge: ${edge.label ?? edge.id}`,
search: "Search workflow items",
connectionTarget: "Choose workflow connection target"
}}
isValidConnection={({ sourceNode, targetNode }) =>
sourceNode.id === targetNode.id ? "Choose a different target." : true
}
/>
</WireProvider>
</ExampleSurface>
}
/>
</DocsPage>
);
}
103 changes: 103 additions & 0 deletions apps/playground/app/docs/examples/controlled-state/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { useState } from "react";
import {
WireWorkspace,
type WireDiagram,
type WireMode,
type WireSelection,
type WireViewport
} from "@aigentive/wire-react";
import { DocsPage } from "../../_components/DocsPage";
import { CodePreview } from "../../_components/CodePreview";
import { Prose, InlineCode } from "../../_components/Prose";
import { ExampleMetric, ExampleSurface, PRODUCTION_DIAGRAM, PRODUCTION_OPTIONS } from "../production-shared";

const CONTROLLED_STATE_SNIPPET = `import "@aigentive/wire-react/styles.css";
import { useState } from "react";
import {
WireWorkspace,
type WireMode,
type WireSelection,
type WireViewport
} from "@aigentive/wire-react";

export function ControlledWorkspace({ initial, optionCatalog }) {
const [diagram, setDiagram] = useState(initial);
const [selection, setSelection] = useState<WireSelection>({ nodeIds: ["qualify"], edgeIds: [] });
const [viewport, setViewport] = useState<WireViewport>({ x: 0, y: 0, zoom: 1 });
const [mode, setMode] = useState<WireMode>("edit");
const [dirty, setDirty] = useState(false);

return (
<WireWorkspace
diagram={diagram}
onChange={setDiagram}
selection={selection}
onSelectionChange={(next) => setSelection(next)}
viewport={viewport}
onViewportChange={(next) => setViewport(next)}
mode={mode}
onModeChange={(next) => setMode(next)}
dirty={dirty}
onDirtyChange={(next) => setDirty(next)}
optionCatalog={optionCatalog}
/>
);
}`;

export default function ControlledStateExamplePage() {
const [diagram, setDiagram] = useState<WireDiagram>(PRODUCTION_DIAGRAM);
const [selection, setSelection] = useState<WireSelection>({ nodeIds: ["qualify"], edgeIds: [] });
const [viewport, setViewport] = useState<WireViewport>({ x: 0, y: 0, zoom: 1 });
const [mode, setMode] = useState<WireMode>("edit");
const [dirty, setDirty] = useState(false);

return (
<DocsPage
eyebrow="Examples"
title="Controlled state"
description="Host the diagram plus runtime selection, viewport, mode, and dirty flags."
crumbs={[{ href: "/", label: "Docs" }, { label: "Examples" }, { label: "Controlled state" }]}
next={{ href: "/examples/edge-inspection", label: "Edge inspection" }}
>
<Prose>
<h2 id="runtime-state">Runtime state</h2>
<p>
<InlineCode>WireDiagram</InlineCode> remains durable. Selection, viewport, mode, and dirty state can be
controlled by the host without becoming persisted diagram fields.
</p>
</Prose>
<CodePreview
snippet={CONTROLLED_STATE_SNIPPET}
height={620}
preview={
<ExampleSurface height={620}>
<div className="grid h-full grid-rows-[auto_minmax(0,1fr)]">
<div className="grid grid-cols-4 gap-2 border-b border-wire bg-wire-sunken p-2">
<ExampleMetric label="Selection" value={`${selection.nodeIds.length} nodes / ${selection.edgeIds.length} edges`} />
<ExampleMetric label="Viewport" value={`${Math.round(viewport.zoom * 100)}%`} />
<ExampleMetric label="Mode" value={mode} />
<ExampleMetric label="Dirty" value={dirty ? "true" : "false"} />
</div>
<WireWorkspace
diagram={diagram}
onChange={setDiagram}
selection={selection}
onSelectionChange={(next) => setSelection(next)}
viewport={viewport}
onViewportChange={(next) => setViewport(next)}
mode={mode}
onModeChange={(next) => setMode(next)}
dirty={dirty}
onDirtyChange={(next) => setDirty(next)}
optionCatalog={PRODUCTION_OPTIONS}
layout="embedded"
/>
</div>
</ExampleSurface>
}
/>
</DocsPage>
);
}
89 changes: 89 additions & 0 deletions apps/playground/app/docs/examples/custom-shell/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import {
WireCanvas,
WireInspector,
WireNodeList,
WirePalette,
WireProvider,
WireToolbar,
WireValidationPanel
} from "@aigentive/wire-react";
import { DocsPage } from "../../_components/DocsPage";
import { CodePreview } from "../../_components/CodePreview";
import { Prose, InlineCode } from "../../_components/Prose";
import { ExampleSurface, PRODUCTION_DIAGRAM, PRODUCTION_OPTIONS } from "../production-shared";

const CUSTOM_SHELL_SNIPPET = `import "@aigentive/wire-react/styles.css";
import {
WireCanvas,
WireInspector,
WireNodeList,
WirePalette,
WireProvider,
WireToolbar,
WireValidationPanel
} from "@aigentive/wire-react";

export function ProductShell({ diagram, onChange, optionCatalog }) {
return (
<WireProvider diagram={diagram} onChange={onChange} validateOnChange>
<WireToolbar />
<div className="grid grid-cols-[220px_minmax(0,1fr)_320px]">
<aside>
<WirePalette />
<WireNodeList />
</aside>
<WireCanvas mode="edit" fitView optionCatalog={optionCatalog} />
<aside>
<WireInspector optionCatalog={optionCatalog} />
<WireValidationPanel />
</aside>
</div>
</WireProvider>
);
}`;

export default function CustomShellExamplePage() {
return (
<DocsPage
eyebrow="Examples"
title="Custom shell"
description="Compose current components around WireProvider when product chrome is custom."
crumbs={[{ href: "/", label: "Docs" }, { label: "Examples" }, { label: "Custom shell" }]}
next={{ href: "/examples/options", label: "Options" }}
>
<Prose>
<h2 id="contract">Contract</h2>
<p>
The shell owns layout. <InlineCode>WireProvider</InlineCode> owns the diagram runtime state, and all edits
still flow through reducer actions against <InlineCode>WireDiagram</InlineCode>.
</p>
</Prose>
<CodePreview
snippet={CUSTOM_SHELL_SNIPPET}
height={560}
preview={
<ExampleSurface height={560}>
<WireProvider defaultDiagram={PRODUCTION_DIAGRAM} validateOnChange>
<div className="grid h-full grid-rows-[auto_minmax(0,1fr)]">
<WireToolbar />
<div className="grid min-h-0 grid-cols-[190px_minmax(0,1fr)_300px]">
<aside className="grid min-h-0 content-start gap-3 border-r border-wire bg-wire-sunken p-3">
<WirePalette />
<WireNodeList />
</aside>
<WireCanvas mode="edit" fitView optionCatalog={PRODUCTION_OPTIONS} />
<aside className="grid min-h-0 content-start gap-3 overflow-auto border-l border-wire bg-wire-sunken p-3">
<WireInspector optionCatalog={PRODUCTION_OPTIONS} />
<WireValidationPanel />
</aside>
</div>
</div>
</WireProvider>
</ExampleSurface>
}
/>
</DocsPage>
);
}
Loading