From a00b4eaec23b772748daae5d542b5b9cadedf2ae Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 20 Aug 2026 09:14:44 +0200 Subject: [PATCH] Flow canvas: mark streaming edges with a marching dash A port with `stream` set publishes repeatedly during one execution, and until now nothing on the canvas said so. `deriveEdges` carries the flag from the producing `MessageSpec` onto the edge, `LiveEdge` turns it into an `edge-stream` class, and the class draws a dashed stroke whose offset marches toward the target. The pulse still lands on top: both animations share one declaration when a value arrives on a streaming edge. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA --- frontend/src/components/Flow/LiveEdge.tsx | 4 ++-- frontend/src/components/Flow/deriveEdges.ts | 12 +++++++--- frontend/src/components/Flow/flow.css | 26 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Flow/LiveEdge.tsx b/frontend/src/components/Flow/LiveEdge.tsx index 8161233..686413f 100644 --- a/frontend/src/components/Flow/LiveEdge.tsx +++ b/frontend/src/components/Flow/LiveEdge.tsx @@ -34,7 +34,7 @@ function LiveEdgeComponent({ data, selected, }: EdgeProps) { - const { message, producerId } = (data ?? {}) as FlowEdgeData + const { message, producerId, stream } = (data ?? {}) as FlowEdgeData const live = useLiveValue(message) const zoom = useStore((state) => state.transform[2]) const [pulsing, setPulsing] = useState(false) @@ -69,7 +69,7 @@ function LiveEdgeComponent({ {live !== undefined && zoom >= CHIP_MIN_ZOOM ? ( diff --git a/frontend/src/components/Flow/deriveEdges.ts b/frontend/src/components/Flow/deriveEdges.ts index 5439d7b..9ead9be 100644 --- a/frontend/src/components/Flow/deriveEdges.ts +++ b/frontend/src/components/Flow/deriveEdges.ts @@ -29,6 +29,8 @@ export type FlowEdgeData = { flow: string /** Whose publication this edge represents, so only it pulses. */ producerId: string + /** The producing port publishes repeatedly during one run, not once at its end. */ + stream?: boolean [key: string]: unknown } @@ -40,14 +42,17 @@ export type FlowEdgeData = { * message therefore draw two edges converging on the same input. */ export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] { - const producers = new Map() + const producers = new Map< + string, + { node: string; port: string; stream?: boolean }[] + >() for (const node of nodes) { for (const spec of node.provides ?? []) { const message = qualify(flow, spec.name ?? "") if (!message) continue const list = producers.get(message) ?? [] - list.push({ node: node.id, port: portOf(spec) }) + list.push({ node: node.id, port: portOf(spec), stream: spec.stream }) producers.set(message, list) } } @@ -71,6 +76,7 @@ export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] { message, flow, producerId: `${flow}.${producer.node}`, + stream: producer.stream, } satisfies FlowEdgeData, }) } @@ -90,7 +96,7 @@ export function bindingsKey(nodes: NodeDef_Input[]): string { `${node.id}|${(node.requires ?? []) .map((s) => `${portOf(s)}=${s.name ?? ""}`) .join(",")}|${(node.provides ?? []) - .map((s) => `${portOf(s)}=${s.name ?? ""}`) + .map((s) => `${portOf(s)}=${s.name ?? ""}${s.stream ? "*" : ""}`) .join(",")}`, ) .join(";") diff --git a/frontend/src/components/Flow/flow.css b/frontend/src/components/Flow/flow.css index 00c5e53..4c1313b 100644 --- a/frontend/src/components/Flow/flow.css +++ b/frontend/src/components/Flow/flow.css @@ -71,6 +71,32 @@ stroke-width: var(--xy-edge-stroke-width); } } + + /* + * An output that publishes repeatedly during one run reads as a march rather + * than a line. Only the dash offset moves, so the pulse above still shows + * through on the same path: it recolours and widens the dashes as they go. + * A loop rather than a one-shot, so none of the `--duration-*` tokens fit — + * the literal is one dash period per step, slow enough to read as direction. + */ + .react-flow__edge-path.edge-stream { + stroke-dasharray: 6 4; + animation: edge-march 700ms linear infinite; + } + + /* Both at once: one `animation` declaration has to carry both, or the + cascade keeps whichever rule came last and drops the other. */ + .react-flow__edge-path.edge-stream.edge-live { + animation: + edge-pulse var(--duration-pulse) var(--ease-emphasized), + edge-march 700ms linear infinite; + } + + @keyframes edge-march { + to { + stroke-dashoffset: -10; + } + } } /* A node that just published something says so, once, and settles. */