{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"use-flow-run","type":"registry:ui","title":"useFlowRun","description":"Executor hook.","package":"fancy-flow","dependencies":[],"registryDependencies":["registry","types"],"files":[{"path":"components\/fancy\/use-flow-run\/use-flow-run.ts","content":"import { useCallback, useRef, useState } from \"react\";\r\nimport { runFlow, type RunOptions, type RunResult } from \".\/run-flow\";\r\nimport { getNodeKind } from \"..\/registry\/registry\";\r\nimport type {\r\n  ExecutorRegistry,\r\n  FlowGraph,\r\n  NodeRunStatus,\r\n  RunEvent,\r\n} from \"..\/types\";\r\n\r\nexport type FlowRunFeedEntry = {\r\n  id: string;\r\n  at: number;\r\n  level: \"info\" | \"warn\" | \"error\" | \"status\";\r\n  text: string;\r\n  nodeId?: string;\r\n  detail?: unknown;\r\n};\r\n\r\nexport type UseFlowRunReturn = {\r\n  \/** Status keyed by nodeId \u2014 drive the UI overlay from this. *\/\r\n  statuses: Record<string, NodeRunStatus>;\r\n  \/** Per-node status text (e.g. error message). *\/\r\n  statusText: Record<string, string | undefined>;\r\n  \/** Latest computed output value per node (for reactive kinds). *\/\r\n  outputs: Record<string, unknown>;\r\n  \/** Live event log (capped to last N). *\/\r\n  feed: FlowRunFeedEntry[];\r\n  \/** Whether a run is currently in progress. *\/\r\n  running: boolean;\r\n  \/** Last run result, or null. *\/\r\n  lastResult: RunResult | null;\r\n  \/** Kick off a run with the provided graph + executors. *\/\r\n  run: (graph: FlowGraph, executors: ExecutorRegistry, options?: RunOptions) => Promise<RunResult>;\r\n  \/** Cancel the current run (if any). *\/\r\n  cancel: () => void;\r\n  \/** Reset all runtime state (statuses, feed, lastResult). *\/\r\n  reset: () => void;\r\n};\r\n\r\nexport type UseFlowRunOptions = {\r\n  \/** Cap the in-memory feed to this many entries. Default 200. *\/\r\n  maxFeed?: number;\r\n};\r\n\r\n\/**\r\n * useFlowRun \u2014 drives `runFlow` + maintains observability state. Pair with\r\n * `applyStatusesToNodes` (below) before passing nodes to `<FlowCanvas>` so\r\n * the per-node status badge renders.\r\n *\/\r\nexport function useFlowRun({ maxFeed = 200 }: UseFlowRunOptions = {}): UseFlowRunReturn {\r\n  const [statuses, setStatuses] = useState<Record<string, NodeRunStatus>>({});\r\n  const [statusText, setStatusText] = useState<Record<string, string | undefined>>({});\r\n  const [outputs, setOutputs] = useState<Record<string, unknown>>({});\r\n  const [feed, setFeed] = useState<FlowRunFeedEntry[]>([]);\r\n  const [running, setRunning] = useState(false);\r\n  const [lastResult, setLastResult] = useState<RunResult | null>(null);\r\n  const abortRef = useRef<AbortController | null>(null);\r\n\r\n  const handleEvent = useCallback(\r\n    (e: RunEvent) => {\r\n      switch (e.type) {\r\n        case \"node-status\":\r\n          setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));\r\n          setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));\r\n          appendFeed({ level: \"status\", text: `${e.nodeId} \u2192 ${e.status}${e.text ? ` (${e.text})` : \"\"}`, nodeId: e.nodeId });\r\n          break;\r\n        case \"node-output\":\r\n          setOutputs((o) => ({ ...o, [e.nodeId]: e.value }));\r\n          appendFeed({ level: \"info\", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });\r\n          break;\r\n        case \"log\":\r\n          appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });\r\n          break;\r\n        case \"run-start\":\r\n          appendFeed({ level: \"info\", text: \"\u25b6 run started\" });\r\n          break;\r\n        case \"run-end\":\r\n          appendFeed({ level: e.ok ? \"info\" : \"error\", text: e.ok ? \"\u2713 run complete\" : \"\u2717 run failed\" });\r\n          break;\r\n        case \"run-error\":\r\n          appendFeed({ level: \"error\", text: e.error });\r\n          break;\r\n      }\r\n      function appendFeed(partial: Omit<FlowRunFeedEntry, \"id\" | \"at\">) {\r\n        setFeed((f) => {\r\n          const entry: FlowRunFeedEntry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };\r\n          const next = [...f, entry];\r\n          return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;\r\n        });\r\n      }\r\n    },\r\n    [maxFeed],\r\n  );\r\n\r\n  const run = useCallback(\r\n    async (graph: FlowGraph, executors: ExecutorRegistry, options: RunOptions = {}) => {\r\n      if (running) {\r\n        return { ok: false, outputs: {}, error: \"another run is already in progress\" } satisfies RunResult;\r\n      }\r\n      const controller = new AbortController();\r\n      abortRef.current = controller;\r\n      \/\/ Reset previous statuses for the nodes we're about to run.\r\n      const idleStatuses: Record<string, NodeRunStatus> = {};\r\n      for (const n of graph.nodes) idleStatuses[n.id] = \"idle\";\r\n      setStatuses(idleStatuses);\r\n      setStatusText({});\r\n      setOutputs({});\r\n      setRunning(true);\r\n      try {\r\n        const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });\r\n        setLastResult(result);\r\n        return result;\r\n      } finally {\r\n        setRunning(false);\r\n        abortRef.current = null;\r\n      }\r\n    },\r\n    [handleEvent, running],\r\n  );\r\n\r\n  const cancel = useCallback(() => abortRef.current?.abort(), []);\r\n\r\n  const reset = useCallback(() => {\r\n    setStatuses({});\r\n    setStatusText({});\r\n    setOutputs({});\r\n    setFeed([]);\r\n    setLastResult(null);\r\n  }, []);\r\n\r\n  return { statuses, statusText, outputs, feed, running, lastResult, run, cancel, reset };\r\n}\r\n\r\n\/**\r\n * Write each reactive kind's latest run output into its `data.output`, so its\r\n * card can render the computed value live. Non-reactive kinds are returned\r\n * untouched. Pair with `applyStatusesToNodes` before rendering.\r\n *\/\r\nexport function applyOutputsToNodes<TNode extends { id: string; type?: string; data: any }>(\r\n  nodes: TNode[],\r\n  outputs: Record<string, unknown>,\r\n): TNode[] {\r\n  return nodes.map((n) => {\r\n    const kind = getNodeKind((n.data as any)?.kind ?? n.type ?? \"\");\r\n    if (!kind?.reactive || !(n.id in outputs)) return n;\r\n    return { ...n, data: { ...n.data, output: outputs[n.id] } };\r\n  });\r\n}\r\n\r\n\/** Merge runtime statuses into nodes for rendering. *\/\r\nexport function applyStatusesToNodes<TNode extends { id: string; data: any }>(\r\n  nodes: TNode[],\r\n  statuses: Record<string, NodeRunStatus>,\r\n  statusText: Record<string, string | undefined>,\r\n): TNode[] {\r\n  return nodes.map((n) => ({\r\n    ...n,\r\n    data: {\r\n      ...n.data,\r\n      status: statuses[n.id] ?? n.data?.status ?? \"idle\",\r\n      statusText: statusText[n.id] ?? n.data?.statusText,\r\n    },\r\n  }));\r\n}\r\n\r\nfunction preview(v: unknown): string {\r\n  try {\r\n    const s = JSON.stringify(v);\r\n    return s && s.length > 60 ? s.slice(0, 57) + \"\u2026\" : (s ?? String(v));\r\n  } catch {\r\n    return String(v);\r\n  }\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/use-flow-run\/use-flow-run.ts"}]}