diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx
new file mode 100644
index 0000000000..447afc9403
--- /dev/null
+++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx
@@ -0,0 +1,91 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, waitFor, cleanup } from "@testing-library/react";
+import type { WorkflowDefinition } from "@fusion/core";
+import { irToFlow, flowToIr, emptyWorkflowIr, emptyWorkflowLayout } from "../workflow-flow-mapping";
+
+vi.mock("../../api", () => ({
+ fetchWorkflows: vi.fn(),
+ createWorkflow: vi.fn(),
+ updateWorkflow: vi.fn(),
+ deleteWorkflow: vi.fn(),
+ compileWorkflow: vi.fn(),
+}));
+
+import { fetchWorkflows } from "../../api";
+import { WorkflowNodeEditor } from "../WorkflowNodeEditor";
+
+function def(): WorkflowDefinition {
+ return {
+ id: "WF-001",
+ name: "QA",
+ description: "",
+ ir: {
+ version: "v1",
+ name: "QA",
+ nodes: [
+ { id: "start", kind: "start" },
+ { id: "lint", kind: "gate", config: { name: "Lint", scriptName: "lint", gateMode: "gate" } },
+ { id: "merge", kind: "prompt", config: { seam: "merge", name: "Merge boundary" } },
+ { id: "end", kind: "end" },
+ ],
+ edges: [
+ { from: "start", to: "lint", condition: "success" },
+ { from: "lint", to: "merge", condition: "success" },
+ { from: "merge", to: "end", condition: "success" },
+ ],
+ },
+ layout: { start: { x: 0, y: 0 }, lint: { x: 120, y: 0 }, merge: { x: 240, y: 0 }, end: { x: 360, y: 0 } },
+ createdAt: "2026-06-03T00:00:00.000Z",
+ updatedAt: "2026-06-03T00:00:00.000Z",
+ };
+}
+
+describe("workflow-flow-mapping", () => {
+ it("round-trips IR through flow and back, preserving structure and layout", () => {
+ const original = def();
+ const flow = irToFlow(original);
+ expect(flow.nodes).toHaveLength(4);
+ expect(flow.nodes.find((n) => n.id === "lint")?.type).toBe("gate");
+ expect(flow.nodes.find((n) => n.id === "merge")?.type).toBe("merge");
+ expect(flow.nodes.find((n) => n.id === "start")?.position).toEqual({ x: 0, y: 0 });
+
+ const { ir, layout } = flowToIr(original.name, flow.nodes, flow.edges);
+ expect(ir.nodes.map((n) => n.id)).toEqual(["start", "lint", "merge", "end"]);
+ // merge marker maps back to a prompt node carrying the seam config.
+ const mergeNode = ir.nodes.find((n) => n.id === "merge");
+ expect(mergeNode?.kind).toBe("prompt");
+ expect(mergeNode?.config?.seam).toBe("merge");
+ expect(ir.edges).toHaveLength(3);
+ expect(layout.lint).toEqual({ x: 120, y: 0 });
+ });
+
+ it("emptyWorkflowIr seeds a connected start→end graph", () => {
+ const ir = emptyWorkflowIr("New");
+ expect(ir.nodes.map((n) => n.kind)).toEqual(["start", "end"]);
+ expect(ir.edges).toEqual([{ from: "start", to: "end", condition: "success" }]);
+ expect(emptyWorkflowLayout().start).toBeDefined();
+ });
+});
+
+describe("WorkflowNodeEditor", () => {
+ beforeEach(() => {
+ vi.mocked(fetchWorkflows).mockResolvedValue([]);
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ it("renders the empty state when there are no workflows (no canvas)", async () => {
+ render(
{}} addToast={() => {}} />);
+ expect(await screen.findByText("Workflows")).toBeInTheDocument();
+ await waitFor(() => expect(screen.getByText(/No workflows yet/i)).toBeInTheDocument());
+ expect(screen.getByText(/Select or create a workflow/i)).toBeInTheDocument();
+ });
+
+ it("renders nothing when closed", () => {
+ const { container } = render( {}} addToast={() => {}} />);
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/packages/dashboard/app/components/nodes/WorkflowNodeTypes.tsx b/packages/dashboard/app/components/nodes/WorkflowNodeTypes.tsx
new file mode 100644
index 0000000000..dec02dc6b1
--- /dev/null
+++ b/packages/dashboard/app/components/nodes/WorkflowNodeTypes.tsx
@@ -0,0 +1,48 @@
+import { Handle, Position, type NodeProps } from "@xyflow/react";
+import { Play, Flag, MessageSquare, Terminal, Shield, GitMerge } from "lucide-react";
+
+/** Node kinds the editor can render. "merge" is the pre/post-merge seam marker. */
+export type WorkflowEditorNodeKind = "start" | "end" | "prompt" | "script" | "gate" | "merge";
+
+export interface WorkflowFlowNodeData {
+ kind: WorkflowEditorNodeKind;
+ label: string;
+ /** Mirrors the IR node config (prompt, scriptName, gateMode, model…). */
+ config?: Record;
+ [key: string]: unknown;
+}
+
+const KIND_ICON: Record = {
+ start: Play,
+ end: Flag,
+ prompt: MessageSquare,
+ script: Terminal,
+ gate: Shield,
+ merge: GitMerge,
+};
+
+function NodeShell({ data, kind }: { data: WorkflowFlowNodeData; kind: WorkflowEditorNodeKind }) {
+ const Icon = KIND_ICON[kind];
+ const showTarget = kind !== "start";
+ const showSource = kind !== "end";
+ return (
+
+ {showTarget && }
+
+
+
+ {data.label || kind}
+ {kind === "gate" && gate}
+ {showSource && }
+
+ );
+}
+
+export const workflowNodeTypes = {
+ start: ({ data }: NodeProps) => ,
+ end: ({ data }: NodeProps) => ,
+ prompt: ({ data }: NodeProps) => ,
+ script: ({ data }: NodeProps) => ,
+ gate: ({ data }: NodeProps) => ,
+ merge: ({ data }: NodeProps) => ,
+};
diff --git a/packages/dashboard/app/components/workflow-flow-mapping.ts b/packages/dashboard/app/components/workflow-flow-mapping.ts
new file mode 100644
index 0000000000..62b9bd5dd8
--- /dev/null
+++ b/packages/dashboard/app/components/workflow-flow-mapping.ts
@@ -0,0 +1,94 @@
+import type { Node as FlowNode, Edge as FlowEdge } from "@xyflow/react";
+import type { WorkflowIr, WorkflowDefinition } from "@fusion/core";
+import type { WorkflowFlowNodeData, WorkflowEditorNodeKind } from "./nodes/WorkflowNodeTypes";
+
+/** Resolve the editor node "type" for an IR node (merge seam → "merge"). */
+function editorKind(node: WorkflowIr["nodes"][number]): WorkflowEditorNodeKind {
+ const seam = node.config?.seam;
+ if (seam === "merge") return "merge";
+ return node.kind;
+}
+
+function nodeLabel(node: WorkflowIr["nodes"][number]): string {
+ const name = node.config?.name;
+ if (typeof name === "string" && name.trim()) return name;
+ if (node.config?.seam === "merge") return "Merge boundary";
+ return node.id;
+}
+
+/** Build React Flow nodes/edges from a stored workflow definition. */
+export function irToFlow(def: WorkflowDefinition): {
+ nodes: FlowNode[];
+ edges: FlowEdge[];
+} {
+ const nodes = def.ir.nodes.map((node, index): FlowNode => {
+ const pos = def.layout?.[node.id];
+ return {
+ id: node.id,
+ type: editorKind(node),
+ position: pos ?? { x: 80 + index * 180, y: 120 },
+ data: { kind: editorKind(node), label: nodeLabel(node), config: { ...(node.config ?? {}) } },
+ deletable: node.kind !== "start" && node.kind !== "end",
+ };
+ });
+
+ const edges = def.ir.edges.map((edge, index): FlowEdge => {
+ const condition = edge.condition ?? "success";
+ return {
+ id: `e-${edge.from}-${edge.to}-${index}`,
+ source: edge.from,
+ target: edge.to,
+ label: condition,
+ data: { condition },
+ };
+ });
+
+ return { nodes, edges };
+}
+
+/** Project React Flow nodes/edges back into a WorkflowIr plus a layout map. */
+export function flowToIr(
+ name: string,
+ nodes: FlowNode[],
+ edges: FlowEdge[],
+): { ir: WorkflowIr; layout: Record } {
+ const irNodes: WorkflowIr["nodes"] = nodes.map((node) => {
+ const data = node.data;
+ const config: Record = { ...(data.config ?? {}) };
+ if (data.label) config.name = data.label;
+ if (data.kind === "merge") {
+ config.seam = "merge";
+ return { id: node.id, kind: "prompt", config };
+ }
+ return { id: node.id, kind: data.kind, config: Object.keys(config).length ? config : undefined };
+ });
+
+ const irEdges: WorkflowIr["edges"] = edges.map((edge) => {
+ const condition = (edge.data?.condition as string | undefined) ?? "success";
+ return { from: edge.source, to: edge.target, condition };
+ });
+
+ const layout = nodes.reduce>((acc, node) => {
+ acc[node.id] = { x: Math.round(node.position.x), y: Math.round(node.position.y) };
+ return acc;
+ }, {});
+
+ return { ir: { version: "v1", name, nodes: irNodes, edges: irEdges }, layout };
+}
+
+/** Seed graph for a brand-new workflow: start → end with room to insert steps. */
+export function emptyWorkflowIr(name: string): WorkflowIr {
+ return {
+ version: "v1",
+ name,
+ nodes: [
+ { id: "start", kind: "start" },
+ { id: "end", kind: "end" },
+ ],
+ edges: [{ from: "start", to: "end", condition: "success" }],
+ };
+}
+
+export function emptyWorkflowLayout(): Record {
+ return { start: { x: 80, y: 140 }, end: { x: 460, y: 140 } };
+}
diff --git a/packages/dashboard/app/hooks/useModalManager.ts b/packages/dashboard/app/hooks/useModalManager.ts
index 281d133703..fc7f6cceb2 100644
--- a/packages/dashboard/app/hooks/useModalManager.ts
+++ b/packages/dashboard/app/hooks/useModalManager.ts
@@ -53,6 +53,7 @@ export interface ModalManager {
activityLogOpen: boolean;
gitManagerOpen: boolean;
workflowStepsOpen: boolean;
+ workflowEditorOpen: boolean;
agentsOpen: boolean;
scriptsOpen: boolean;
setupWizardOpen: boolean;
@@ -117,6 +118,8 @@ export interface ModalManager {
openWorkflowSteps: () => void;
closeWorkflowSteps: () => void;
+ openWorkflowEditor: () => void;
+ closeWorkflowEditor: () => void;
openAgents: () => void;
closeAgents: () => void;
@@ -173,6 +176,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const [activityLogOpen, setActivityLogOpen] = useState(false);
const [gitManagerOpen, setGitManagerOpen] = useState(false);
const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false);
+ const [workflowEditorOpen, setWorkflowEditorOpen] = useState(false);
const [agentsOpen, setAgentsOpen] = useState(false);
const [scriptsOpen, setScriptsOpen] = useState(false);
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
@@ -191,6 +195,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
activityLogOpen ||
gitManagerOpen ||
workflowStepsOpen ||
+ workflowEditorOpen ||
scriptsOpen ||
agentsOpen ||
usageOpen ||
@@ -340,6 +345,8 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
const openWorkflowSteps = useCallback(() => setWorkflowStepsOpen(true), []);
const closeWorkflowSteps = useCallback(() => setWorkflowStepsOpen(false), []);
+ const openWorkflowEditor = useCallback(() => setWorkflowEditorOpen(true), []);
+ const closeWorkflowEditor = useCallback(() => setWorkflowEditorOpen(false), []);
const openAgents = useCallback(() => setAgentsOpen(true), []);
const closeAgents = useCallback(() => setAgentsOpen(false), []);
@@ -406,6 +413,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
activityLogOpen,
gitManagerOpen,
workflowStepsOpen,
+ workflowEditorOpen,
agentsOpen,
scriptsOpen,
setupWizardOpen,
@@ -450,6 +458,8 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
closeGitManager,
openWorkflowSteps,
closeWorkflowSteps,
+ openWorkflowEditor,
+ closeWorkflowEditor,
openAgents,
closeAgents,
openScripts,
diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json
index 436fd0fb7a..c3385a3965 100644
--- a/packages/dashboard/package.json
+++ b/packages/dashboard/package.json
@@ -90,23 +90,24 @@
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.4",
+ "@earendil-works/pi-coding-agent": "^0.78.0",
+ "@fusion-plugin-examples/cli-printing-press": "workspace:*",
+ "@fusion-plugin-examples/cursor-runtime": "workspace:*",
"@fusion-plugin-examples/dependency-graph": "workspace:*",
- "@fusion-plugin-examples/roadmap": "workspace:*",
+ "@fusion-plugin-examples/droid-runtime": "workspace:*",
"@fusion-plugin-examples/hermes-runtime": "workspace:*",
"@fusion-plugin-examples/openclaw-runtime": "workspace:*",
- "@fusion-plugin-examples/droid-runtime": "workspace:*",
- "@fusion-plugin-examples/cursor-runtime": "workspace:*",
- "@fusion-plugin-examples/cli-printing-press": "workspace:*",
"@fusion-plugin-examples/paperclip-runtime": "workspace:*",
+ "@fusion-plugin-examples/roadmap": "workspace:*",
"@fusion/core": "workspace:*",
"@fusion/engine": "workspace:*",
- "@earendil-works/pi-coding-agent": "^0.78.0",
"@types/multer": "^2.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
+ "@xyflow/react": "^12.11.0",
"archiver": "^7.0.1",
"express": "^5.1.0",
"ioredis": "^5.6.0",
diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts
index 3fb2ca4ea6..7032b9f8e5 100644
--- a/packages/dashboard/vite.config.ts
+++ b/packages/dashboard/vite.config.ts
@@ -172,6 +172,10 @@ export default defineConfig({
return "vendor-codemirror";
}
+ if (id.includes("/node_modules/@xyflow/")) {
+ return "vendor-reactflow";
+ }
+
return undefined;
},
},
diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts
index 302be7480b..cfa7a7846e 100644
--- a/packages/dashboard/vitest.config.ts
+++ b/packages/dashboard/vitest.config.ts
@@ -171,6 +171,7 @@ const qualityAppComponentTests = [
"TaskForm",
"TaskIdIntegrityBanner",
"TrackingRepoSelect",
+ "WorkflowNodeEditor",
"WorkflowResultsTab",
"WorktrunkInstallApprovalDetails",
] as const;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4df8b1098a..3d351b80ea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,10 +43,10 @@ importers:
dependencies:
'@earendil-works/pi-ai':
specifier: ^0.78.0
- version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
+ version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
'@earendil-works/pi-coding-agent':
specifier: ^0.78.0
- version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
+ version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
dockerode:
specifier: ^4.0.12
version: 4.0.12
@@ -254,6 +254,9 @@ importers:
'@xterm/xterm':
specifier: ^5.5.0
version: 5.5.0
+ '@xyflow/react':
+ specifier: ^12.11.0
+ version: 12.11.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
archiver:
specifier: ^7.0.1
version: 7.0.1
@@ -516,10 +519,10 @@ importers:
dependencies:
'@earendil-works/pi-ai':
specifier: '*'
- version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-coding-agent':
specifier: '*'
- version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
devDependencies:
'@types/node':
specifier: ^25.5.2
@@ -2518,6 +2521,24 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
@@ -2804,6 +2825,22 @@ packages:
'@xterm/xterm@5.5.0':
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
+ '@xyflow/react@12.11.0':
+ resolution: {integrity: sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==}
+ peerDependencies:
+ '@types/react': '>=17'
+ '@types/react-dom': '>=17'
+ react: '>=17'
+ react-dom: '>=17'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@xyflow/system@0.0.77':
+ resolution: {integrity: sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==}
+
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -3251,6 +3288,9 @@ packages:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
+ classcat@5.0.5:
+ resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
+
cli-boxes@3.0.0:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
@@ -3440,6 +3480,44 @@ packages:
curve25519-js@0.0.4:
resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==}
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
@@ -6135,6 +6213,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
@@ -6419,6 +6502,21 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ zustand@4.5.7:
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -7158,9 +7256,9 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
+ '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
- '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
ignore: 7.0.5
typebox: 1.1.38
yaml: 2.9.0
@@ -7172,9 +7270,9 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-agent-core@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
+ '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
- '@earendil-works/pi-ai': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
+ '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
ignore: 7.0.5
typebox: 1.1.38
yaml: 2.9.0
@@ -7234,16 +7332,16 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
+ '@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
- '@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
+ '@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
- '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
+ '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))
'@mistralai/mistralai': 2.2.1
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
- openai: 6.26.0(ws@8.20.0)(zod@4.3.6)
+ openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies:
@@ -7254,16 +7352,16 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-ai@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
+ '@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
- '@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
+ '@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
- '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))
+ '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
'@mistralai/mistralai': 2.2.1
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
- openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
+ openai: 6.26.0(ws@8.20.0)(zod@4.3.6)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies:
@@ -7343,10 +7441,10 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
+ '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
- '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
- '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
+ '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-tui': 0.77.0
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
@@ -7372,11 +7470,11 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-coding-agent@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
+ '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
- '@earendil-works/pi-agent-core': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
- '@earendil-works/pi-ai': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
- '@earendil-works/pi-tui': 0.78.0
+ '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ '@earendil-works/pi-tui': 0.77.0
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cross-spawn: 7.0.6
@@ -8527,6 +8625,27 @@ snapshots:
dependencies:
'@types/node': 25.5.2
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
'@types/debug@4.1.13':
dependencies:
'@types/ms': 2.1.0
@@ -8934,6 +9053,31 @@ snapshots:
'@xterm/xterm@5.5.0': {}
+ '@xyflow/react@12.11.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@xyflow/system': 0.0.77
+ classcat: 5.0.5
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+ transitivePeerDependencies:
+ - immer
+
+ '@xyflow/system@0.0.77':
+ dependencies:
+ '@types/d3-drag': 3.0.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+
abbrev@3.0.1: {}
abort-controller@3.0.0:
@@ -9427,6 +9571,8 @@ snapshots:
ci-info@4.4.0: {}
+ classcat@5.0.5: {}
+
cli-boxes@3.0.0: {}
cli-cursor@3.1.0:
@@ -9600,6 +9746,42 @@ snapshots:
curve25519-js@0.0.4: {}
+ d3-color@3.1.0: {}
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-ease@3.0.1: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
data-uri-to-buffer@4.0.1: {}
data-urls@7.0.0:
@@ -12831,6 +13013,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {}
@@ -13176,4 +13362,11 @@ snapshots:
zod@4.3.6: {}
+ zustand@4.5.7(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ react: 19.2.4
+
zwitch@2.0.4: {}