feat(dashboard): visual graph node editor for workflows (U6, U7)
Add a React Flow (@xyflow/react) based WorkflowNodeEditor: a lazy-loaded modal with a workflow list, a node palette (prompt/script/gate/merge-boundary), drag-to-connect edges, a per-node inspector, and save with compile-validation that surfaces non-linear graphs as a banner. Pure irToFlow/flowToIr mapping round-trips the v1 IR plus editor layout. Reachable via a Graph editor button in the Workflow Steps manager; mounted in AppModals behind a new modal-manager flag. Adds a vendor-reactflow Vite chunk and a feature changeset.
This commit is contained in:
5
.changeset/graph-custom-workflows.md
Normal file
5
.changeset/graph-custom-workflows.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Add executable custom workflows with a visual graph node editor. Author a workflow as a graph (start → prompt/script/gate steps → end) in a new React Flow–based editor, then select it per task or set a project default. Selected workflows compile to the existing WorkflowStep engine and run at the pre/post-merge boundaries — no changes to the scheduler/executor/merger. Non-linear graphs are rejected with a clear message and reserved for the (deferred) graph interpreter.
|
||||
@@ -29,6 +29,7 @@ import { useNavigationHistoryContext } from "../hooks/useNavigationHistory";
|
||||
|
||||
const SetupWizardModal = lazy(() => import("./SetupWizardModal").then((m) => ({ default: m.SetupWizardModal })));
|
||||
const SettingsModal = lazy(() => import("./SettingsModal").then((m) => ({ default: m.SettingsModal })));
|
||||
const WorkflowNodeEditor = lazy(() => import("./WorkflowNodeEditor").then((m) => ({ default: m.WorkflowNodeEditor })));
|
||||
|
||||
function prefetchSettingsModal() {
|
||||
const idle: (cb: () => void, opts?: { timeout?: number }) => number =
|
||||
@@ -378,9 +379,26 @@ export function AppModals({
|
||||
onClose={modalManager.closeWorkflowSteps}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
onOpenGraphEditor={() => {
|
||||
modalManager.closeWorkflowSteps();
|
||||
modalManager.openWorkflowEditor();
|
||||
}}
|
||||
/>
|
||||
</ModalErrorBoundary>
|
||||
|
||||
{modalManager.workflowEditorOpen && (
|
||||
<ModalErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<WorkflowNodeEditor
|
||||
isOpen={modalManager.workflowEditorOpen}
|
||||
onClose={modalManager.closeWorkflowEditor}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</Suspense>
|
||||
</ModalErrorBoundary>
|
||||
)}
|
||||
|
||||
<AgentListModal
|
||||
isOpen={modalManager.agentsOpen}
|
||||
onClose={modalManager.closeAgents}
|
||||
|
||||
297
packages/dashboard/app/components/WorkflowNodeEditor.css
Normal file
297
packages/dashboard/app/components/WorkflowNodeEditor.css
Normal file
@@ -0,0 +1,297 @@
|
||||
.wf-editor-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(1200px, 95vw);
|
||||
height: min(820px, 92vh);
|
||||
min-width: 640px;
|
||||
min-height: 480px;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.wf-editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-md);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wf-editor-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wf-editor-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wf-editor-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wf-editor-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wf-editor-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
width: 220px;
|
||||
padding: var(--space-sm);
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wf-editor-new {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wf-editor-new:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.wf-editor-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wf-editor-list-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wf-editor-list-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.wf-editor-list-item.active {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.wf-editor-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-md);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wf-editor-canvas-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wf-editor-canvas-empty {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wf-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wf-editor-palette {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wf-palette-btn,
|
||||
.wf-editor-delete,
|
||||
.wf-editor-save {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.wf-palette-btn:hover,
|
||||
.wf-editor-delete:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.wf-editor-actions {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.wf-editor-save {
|
||||
background: var(--todo);
|
||||
border-color: var(--todo);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.wf-editor-save:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wf-editor-delete {
|
||||
color: var(--ws-error);
|
||||
}
|
||||
|
||||
.wf-editor-banner {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--ws-warning);
|
||||
color: var(--ws-warning);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wf-editor-canvas {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wf-editor-inspector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
width: 280px;
|
||||
padding: var(--space-md);
|
||||
border-left: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wf-editor-inspector h3 {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wf-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.wf-field input,
|
||||
.wf-field textarea,
|
||||
.wf-field select {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.wf-field input:focus,
|
||||
.wf-field textarea:focus,
|
||||
.wf-field select:focus {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.wf-inspector-note {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Canvas nodes */
|
||||
.wf-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wf-node-start {
|
||||
border-color: var(--ws-success);
|
||||
}
|
||||
|
||||
.wf-node-end {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.wf-node-gate {
|
||||
border-color: var(--ws-warning);
|
||||
}
|
||||
|
||||
.wf-node-merge {
|
||||
border-color: var(--ws-info);
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.wf-node-icon {
|
||||
display: inline-flex;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.wf-node-badge {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
padding: 1px var(--space-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--ws-warning);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.wf-spin {
|
||||
animation: wf-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wf-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
359
packages/dashboard/app/components/WorkflowNodeEditor.tsx
Normal file
359
packages/dashboard/app/components/WorkflowNodeEditor.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import "./WorkflowNodeEditor.css";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
type Connection,
|
||||
type Node as FlowNode,
|
||||
type Edge as FlowEdge,
|
||||
} from "@xyflow/react";
|
||||
import { X, Plus, Trash2, Save, MessageSquare, Terminal, Shield, GitMerge, Loader2 } from "lucide-react";
|
||||
import type { WorkflowDefinition } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import {
|
||||
fetchWorkflows,
|
||||
createWorkflow,
|
||||
updateWorkflow,
|
||||
deleteWorkflow,
|
||||
compileWorkflow,
|
||||
} from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { workflowNodeTypes, type WorkflowFlowNodeData, type WorkflowEditorNodeKind } from "./nodes/WorkflowNodeTypes";
|
||||
import { irToFlow, flowToIr, emptyWorkflowIr, emptyWorkflowLayout } from "./workflow-flow-mapping";
|
||||
|
||||
interface WorkflowNodeEditorProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
let nodeSeq = 0;
|
||||
function newNodeId(): string {
|
||||
nodeSeq += 1;
|
||||
return `n-${Date.now().toString(36)}-${nodeSeq}`;
|
||||
}
|
||||
|
||||
const PALETTE: Array<{ kind: WorkflowEditorNodeKind; label: string; icon: typeof MessageSquare }> = [
|
||||
{ kind: "prompt", label: "Prompt", icon: MessageSquare },
|
||||
{ kind: "script", label: "Script", icon: Terminal },
|
||||
{ kind: "gate", label: "Gate", icon: Shield },
|
||||
{ kind: "merge", label: "Merge boundary", icon: GitMerge },
|
||||
];
|
||||
|
||||
function InnerEditor({
|
||||
onClose,
|
||||
addToast,
|
||||
projectId,
|
||||
modalRef,
|
||||
}: Omit<WorkflowNodeEditorProps, "isOpen"> & { modalRef: React.RefObject<HTMLDivElement | null> }) {
|
||||
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNode<WorkflowFlowNodeData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<FlowEdge>([]);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
|
||||
const activeWorkflow = useMemo(() => workflows.find((w) => w.id === activeId), [workflows, activeId]);
|
||||
|
||||
const loadWorkflows = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchWorkflows(projectId);
|
||||
setWorkflows(data);
|
||||
setActiveId((prev) => prev ?? data[0]?.id ?? null);
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to load workflows", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, addToast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadWorkflows();
|
||||
}, [loadWorkflows]);
|
||||
|
||||
// Load the active workflow graph into the canvas.
|
||||
useEffect(() => {
|
||||
if (!activeWorkflow) {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
return;
|
||||
}
|
||||
const flow = irToFlow(activeWorkflow);
|
||||
setNodes(flow.nodes);
|
||||
setEdges(flow.edges);
|
||||
setSelectedNodeId(null);
|
||||
setValidationError(null);
|
||||
}, [activeWorkflow, setNodes, setEdges]);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
setEdges((eds) =>
|
||||
addEdge({ ...connection, label: "success", data: { condition: "success" } }, eds),
|
||||
);
|
||||
},
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
const addNode = useCallback(
|
||||
(kind: WorkflowEditorNodeKind) => {
|
||||
const id = newNodeId();
|
||||
const label = kind === "merge" ? "Merge boundary" : kind.charAt(0).toUpperCase() + kind.slice(1);
|
||||
setNodes((ns) => [
|
||||
...ns,
|
||||
{
|
||||
id,
|
||||
type: kind,
|
||||
position: { x: 200 + ns.length * 40, y: 240 + (ns.length % 3) * 70 },
|
||||
data: { kind, label, config: kind === "gate" ? { gateMode: "gate" } : {} },
|
||||
deletable: true,
|
||||
},
|
||||
]);
|
||||
setSelectedNodeId(id);
|
||||
},
|
||||
[setNodes],
|
||||
);
|
||||
|
||||
const updateSelectedData = useCallback(
|
||||
(patch: Partial<WorkflowFlowNodeData> | { config: Record<string, unknown> }) => {
|
||||
if (!selectedNodeId) return;
|
||||
setNodes((ns) =>
|
||||
ns.map((n) =>
|
||||
n.id === selectedNodeId
|
||||
? {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
...("config" in patch ? { config: { ...n.data.config, ...patch.config } } : patch),
|
||||
},
|
||||
}
|
||||
: n,
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedNodeId, setNodes],
|
||||
);
|
||||
|
||||
const handleCreateWorkflow = useCallback(async () => {
|
||||
const name = window.prompt("New workflow name");
|
||||
if (!name?.trim()) return;
|
||||
try {
|
||||
const created = await createWorkflow(
|
||||
{ name: name.trim(), ir: emptyWorkflowIr(name.trim()), layout: emptyWorkflowLayout() },
|
||||
projectId,
|
||||
);
|
||||
setWorkflows((ws) => [...ws, created]);
|
||||
setActiveId(created.id);
|
||||
addToast(`Created workflow "${created.name}"`, "success");
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to create workflow", "error");
|
||||
}
|
||||
}, [projectId, addToast]);
|
||||
|
||||
const handleDeleteWorkflow = useCallback(async () => {
|
||||
if (!activeWorkflow) return;
|
||||
if (!window.confirm(`Delete workflow "${activeWorkflow.name}"?`)) return;
|
||||
try {
|
||||
await deleteWorkflow(activeWorkflow.id, projectId);
|
||||
setWorkflows((ws) => ws.filter((w) => w.id !== activeWorkflow.id));
|
||||
setActiveId(null);
|
||||
addToast("Workflow deleted", "success");
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to delete workflow", "error");
|
||||
}
|
||||
}, [activeWorkflow, projectId, addToast]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!activeWorkflow) return;
|
||||
setSaving(true);
|
||||
setValidationError(null);
|
||||
try {
|
||||
const { ir, layout } = flowToIr(activeWorkflow.name, nodes, edges);
|
||||
const updated = await updateWorkflow(activeWorkflow.id, { ir, layout }, projectId);
|
||||
setWorkflows((ws) => ws.map((w) => (w.id === updated.id ? updated : w)));
|
||||
// Validate by compiling — surfaces non-linear graphs as a banner.
|
||||
try {
|
||||
await compileWorkflow(updated.id, projectId);
|
||||
addToast("Workflow saved", "success");
|
||||
} catch (compileErr) {
|
||||
setValidationError(getErrorMessage(compileErr) || "Workflow saved but cannot be compiled");
|
||||
}
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err) || "Failed to save workflow";
|
||||
setValidationError(message);
|
||||
addToast(message, "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [activeWorkflow, nodes, edges, projectId, addToast]);
|
||||
|
||||
const selectedNode = nodes.find((n) => n.id === selectedNodeId) ?? null;
|
||||
const overlayProps = useOverlayDismiss(onClose);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay wf-editor-overlay" {...overlayProps}>
|
||||
<div className="modal wf-editor-modal" ref={modalRef} onClick={(e) => e.stopPropagation()}>
|
||||
<header className="wf-editor-header">
|
||||
<h2>Workflows</h2>
|
||||
<button className="wf-editor-close" onClick={onClose} aria-label="Close workflow editor">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="wf-editor-body">
|
||||
<aside className="wf-editor-sidebar">
|
||||
<button className="wf-editor-new" onClick={handleCreateWorkflow}>
|
||||
<Plus size={14} /> New workflow
|
||||
</button>
|
||||
{loading ? (
|
||||
<div className="wf-editor-empty">
|
||||
<Loader2 size={16} className="wf-spin" /> Loading…
|
||||
</div>
|
||||
) : workflows.length === 0 ? (
|
||||
<div className="wf-editor-empty">No workflows yet.</div>
|
||||
) : (
|
||||
<ul className="wf-editor-list">
|
||||
{workflows.map((w) => (
|
||||
<li key={w.id}>
|
||||
<button
|
||||
className={`wf-editor-list-item${w.id === activeId ? " active" : ""}`}
|
||||
onClick={() => setActiveId(w.id)}
|
||||
>
|
||||
{w.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<section className="wf-editor-canvas-wrap">
|
||||
{activeWorkflow ? (
|
||||
<>
|
||||
<div className="wf-editor-toolbar">
|
||||
<div className="wf-editor-palette">
|
||||
{PALETTE.map(({ kind, label, icon: Icon }) => (
|
||||
<button key={kind} className="wf-palette-btn" onClick={() => addNode(kind)}>
|
||||
<Icon size={13} /> {label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="wf-editor-actions">
|
||||
<button className="wf-editor-delete" onClick={handleDeleteWorkflow}>
|
||||
<Trash2 size={13} /> Delete
|
||||
</button>
|
||||
<button className="wf-editor-save" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 size={13} className="wf-spin" /> : <Save size={13} />} Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{validationError && (
|
||||
<div className="wf-editor-banner" role="alert">
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="wf-editor-canvas">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={workflowNodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={(_, node) => setSelectedNodeId(node.id)}
|
||||
onPaneClick={() => setSelectedNodeId(null)}
|
||||
fitView
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap pannable zoomable />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="wf-editor-empty wf-editor-canvas-empty">
|
||||
Select or create a workflow to start editing.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{selectedNode && selectedNode.data.kind !== "start" && selectedNode.data.kind !== "end" && (
|
||||
<aside className="wf-editor-inspector">
|
||||
<h3>Node</h3>
|
||||
<label className="wf-field">
|
||||
<span>Name</span>
|
||||
<input
|
||||
value={selectedNode.data.label}
|
||||
onChange={(e) => updateSelectedData({ label: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{selectedNode.data.kind === "prompt" || selectedNode.data.kind === "gate" ? (
|
||||
<label className="wf-field">
|
||||
<span>Prompt</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={String(selectedNode.data.config?.prompt ?? "")}
|
||||
onChange={(e) => updateSelectedData({ config: { prompt: e.target.value } })}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{selectedNode.data.kind === "script" ? (
|
||||
<label className="wf-field">
|
||||
<span>Script name</span>
|
||||
<input
|
||||
value={String(selectedNode.data.config?.scriptName ?? "")}
|
||||
onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{selectedNode.data.kind !== "merge" ? (
|
||||
<label className="wf-field">
|
||||
<span>Gate mode</span>
|
||||
<select
|
||||
value={String(selectedNode.data.config?.gateMode ?? (selectedNode.data.kind === "gate" ? "gate" : "advisory"))}
|
||||
onChange={(e) => updateSelectedData({ config: { gateMode: e.target.value } })}
|
||||
>
|
||||
<option value="advisory">Advisory</option>
|
||||
<option value="gate">Gate (blocks)</option>
|
||||
</select>
|
||||
</label>
|
||||
) : (
|
||||
<p className="wf-inspector-note">
|
||||
Steps before this marker run pre-merge; steps after run post-merge.
|
||||
</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowNodeEditor({ isOpen, ...rest }: WorkflowNodeEditorProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
useModalResizePersist(modalRef, isOpen, "fusion:workflow-node-editor-size");
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<InnerEditor {...rest} modalRef={modalRef} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -542,3 +542,25 @@
|
||||
padding: 12px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.wfm-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.wfm-graph-editor-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfm-graph-editor-link:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ interface WorkflowStepManagerProps {
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
/** Optional: open the visual graph workflow editor. */
|
||||
onOpenGraphEditor?: () => void;
|
||||
}
|
||||
|
||||
interface StepFormData {
|
||||
@@ -127,7 +129,7 @@ function getCategoryClassName(category: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: WorkflowStepManagerProps) {
|
||||
export function WorkflowStepManager({ isOpen, onClose, addToast, projectId, onOpenGraphEditor }: WorkflowStepManagerProps) {
|
||||
const [steps, setSteps] = useState<WorkflowStep[]>([]);
|
||||
const [templates, setTemplates] = useState<WorkflowStepTemplate[]>([]);
|
||||
const [pluginTemplateOwners, setPluginTemplateOwners] = useState<Record<string, string>>({});
|
||||
@@ -422,9 +424,16 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<h2>Workflow Steps</h2>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
<div className="wfm-header-actions">
|
||||
{onOpenGraphEditor && (
|
||||
<button className="wfm-graph-editor-link" onClick={onOpenGraphEditor}>
|
||||
<LayoutGrid size={14} /> Graph editor
|
||||
</button>
|
||||
)}
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wfm-body">
|
||||
|
||||
@@ -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(<WorkflowNodeEditor isOpen onClose={() => {}} 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(<WorkflowNodeEditor isOpen={false} onClose={() => {}} addToast={() => {}} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -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<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const KIND_ICON: Record<WorkflowEditorNodeKind, typeof Play> = {
|
||||
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 (
|
||||
<div className={`wf-node wf-node-${kind}`} data-testid={`wf-node-${kind}`}>
|
||||
{showTarget && <Handle type="target" position={Position.Left} />}
|
||||
<span className="wf-node-icon">
|
||||
<Icon size={14} aria-hidden />
|
||||
</span>
|
||||
<span className="wf-node-label">{data.label || kind}</span>
|
||||
{kind === "gate" && <span className="wf-node-badge">gate</span>}
|
||||
{showSource && <Handle type="source" position={Position.Right} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const workflowNodeTypes = {
|
||||
start: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="start" />,
|
||||
end: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="end" />,
|
||||
prompt: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="prompt" />,
|
||||
script: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="script" />,
|
||||
gate: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="gate" />,
|
||||
merge: ({ data }: NodeProps) => <NodeShell data={data as WorkflowFlowNodeData} kind="merge" />,
|
||||
};
|
||||
94
packages/dashboard/app/components/workflow-flow-mapping.ts
Normal file
94
packages/dashboard/app/components/workflow-flow-mapping.ts
Normal file
@@ -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<WorkflowFlowNodeData>[];
|
||||
edges: FlowEdge[];
|
||||
} {
|
||||
const nodes = def.ir.nodes.map((node, index): FlowNode<WorkflowFlowNodeData> => {
|
||||
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<WorkflowFlowNodeData>[],
|
||||
edges: FlowEdge[],
|
||||
): { ir: WorkflowIr; layout: Record<string, { x: number; y: number }> } {
|
||||
const irNodes: WorkflowIr["nodes"] = nodes.map((node) => {
|
||||
const data = node.data;
|
||||
const config: Record<string, unknown> = { ...(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<Record<string, { x: number; y: number }>>((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<string, { x: number; y: number }> {
|
||||
return { start: { x: 80, y: 140 }, end: { x: 460, y: 140 } };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -172,6 +172,10 @@ export default defineConfig({
|
||||
return "vendor-codemirror";
|
||||
}
|
||||
|
||||
if (id.includes("/node_modules/@xyflow/")) {
|
||||
return "vendor-reactflow";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -171,6 +171,7 @@ const qualityAppComponentTests = [
|
||||
"TaskForm",
|
||||
"TaskIdIntegrityBanner",
|
||||
"TrackingRepoSelect",
|
||||
"WorkflowNodeEditor",
|
||||
"WorkflowResultsTab",
|
||||
"WorktrunkInstallApprovalDetails",
|
||||
] as const;
|
||||
|
||||
239
pnpm-lock.yaml
generated
239
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user