Add flow settings, pulse emitting nodes, and simplify node state

- One dot per node now carries the whole story: primary while running, sage
  after a good run, red when anything is wrong, with the explanation on hover.
  The corner badge is gone, along with the second way of saying the same thing.
- A node that publishes something flashes a ring, so a running flow is legible
  without reading the edge values. Nodes that consume but publish nothing stay
  quiet, which is why the event carries an output count.
- Flow settings open in the same panel its nodes use, from a pencil in the
  dock: the title, the name, and deleting the flow. NodePanel and FlowPanel
  share the panel chrome rather than each drawing their own.
- Renaming is a server operation, because a flow's name is the namespace of its
  messages: the directory moves and every other flow reading `old.message` is
  repointed, instead of being left pointing at a flow that no longer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
Melvin Strobl
2026-08-15 19:46:23 +02:00
co-authored by Claude Fable 5
parent c254d487ba
commit fd666743d2
18 changed files with 756 additions and 197 deletions
+15 -34
View File
@@ -1,13 +1,5 @@
import { Handle, type NodeProps, Position } from "@xyflow/react"
import {
AlertCircle,
Braces,
Clock,
Code2,
Database,
Globe,
Radio,
} from "lucide-react"
import { Braces, Clock, Code2, Database, Globe, Radio } from "lucide-react"
import { memo } from "react"
import type { MessageSpec, NodeDef_Input } from "@/client"
@@ -18,7 +10,7 @@ import {
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { portOf } from "./deriveEdges"
import { useNodeStatus } from "./liveStore"
import { useNodeEmits, useNodeStatus } from "./liveStore"
const NODE_ICONS = {
python: Code2,
@@ -29,11 +21,12 @@ const NODE_ICONS = {
mlp: Braces,
} as const
// Only the states worth a quiet marker. Anything wrong goes to the badge
// instead, so a problem is never reported twice on the same node.
// One dot says everything about a node's state. Idle nodes carry no dot at all,
// so the canvas stays quiet until something happens.
const STATUS_STYLES = {
running: { dot: "bg-primary animate-pulse", label: "Running" },
success: { dot: "bg-status-success", label: "Last run succeeded" },
error: { dot: "bg-destructive", label: "Something went wrong" },
} as const
export type FlowNodeData = {
@@ -85,16 +78,16 @@ function PortHandles({
function FlowNodeComponent({ data, selected }: NodeProps) {
const { definition, flow, typeLabel, issueText } = data as FlowNodeData
const live = useNodeStatus(`${flow}.${definition.id}`)
const emits = useNodeEmits(`${flow}.${definition.id}`)
const Icon = NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ?? Code2
// Whatever is wrong — it failed to load, it failed to run, or the graph
// around it does not add up — is one badge with one explanation.
// around it does not add up — is the same red dot with the same explanation.
const problem = [live?.status === "error" ? live.error : null, issueText]
.filter(Boolean)
.join("\n")
const style = problem
? undefined
: STATUS_STYLES[live?.status as keyof typeof STATUS_STYLES]
const status = problem ? "error" : live?.status
const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES]
return (
<div
@@ -103,6 +96,9 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
selected && "border-primary shadow-e2",
)}
>
{/* Remounting on each emit is what restarts the animation. */}
{emits > 0 ? <span key={emits} className="node-pulse" /> : null}
<PortHandles
specs={definition.requires ?? []}
type="target"
@@ -130,28 +126,13 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
aria-label={style.label}
/>
</TooltipTrigger>
<TooltipContent>{live?.error ?? style.label}</TooltipContent>
<TooltipContent className="max-w-xs whitespace-pre-line">
{problem || style.label}
</TooltipContent>
</Tooltip>
) : null}
</div>
{problem ? (
<Tooltip>
<TooltipTrigger asChild>
<span
role="img"
aria-label="This node has a problem"
className="absolute -right-1.5 -top-1.5 flex size-4 items-center justify-center rounded-full bg-destructive text-primary-foreground"
>
<AlertCircle className="size-3" />
</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs whitespace-pre-line">
{problem}
</TooltipContent>
</Tooltip>
) : null}
<PortHandles
specs={definition.provides ?? []}
type="source"