Keep the node ring lapping while the node is running

The arc was one-shot feedback for an emit, so a node that works for minutes
showed nothing after the first half second. A run now holds the ring on the
node and laps it until the node stops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
This commit is contained in:
2026-08-20 14:43:31 +02:00
co-authored by Claude Opus 5
parent 3e95842b5f
commit 26833d63ee
2 changed files with 31 additions and 4 deletions
+11 -3
View File
@@ -152,6 +152,7 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
.split("\n") .split("\n")
const problem = [...new Set(lines)].join("\n") const problem = [...new Set(lines)].join("\n")
const status = problem ? "error" : live?.status const status = problem ? "error" : live?.status
const running = status === "running"
const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES] const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES]
return ( return (
@@ -160,8 +161,15 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
<div className="relative rounded-lg"> <div className="relative rounded-lg">
{/* Remounting on each emit is what restarts the animation, so this is {/* Remounting on each emit is what restarts the animation, so this is
gated on the pulse rather than on the count: a node mounting with a gated on the pulse rather than on the count: a node mounting with a
count already in the store would otherwise play it once for free. */} count already in the store would otherwise play it once for free.
{firing ? <span key={emits} className="node-pulse" /> : null} A run is not one shot and has no length known in advance, so while one
is on the ring laps under a key of its own until the node stops. */}
{running || firing ? (
<span
key={running ? "run" : emits}
className={cn("node-pulse", running && "node-pulse-run")}
/>
) : null}
<div <div
className={cn( className={cn(
"relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow", "relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow",
@@ -186,7 +194,7 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
{typeLabel} {typeLabel}
</span> </span>
</span> </span>
{status === "running" ? ( {running ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
+20 -1
View File
@@ -109,7 +109,8 @@
/* /*
* A node that just published something says so, once, and settles: an arc runs * A node that just published something says so, once, and settles: an arc runs
* one lap of the rim and goes out. * one lap of the rim and goes out. The same arc, lapping, is a node still at
* work.
* *
* The angle has to be registered to be animatable at all — an unregistered * The angle has to be registered to be animatable at all — an unregistered
* custom property is a token stream, and swapping one for another mid-gradient * custom property is a token stream, and swapping one for another mid-gradient
@@ -169,6 +170,24 @@
opacity: 0; opacity: 0;
} }
} }
/*
* A node still working keeps the arc going instead: a run lasts as long as it
* lasts — a training is minutes — so the lap repeats and never fades out, and
* the ring is taken down when the node stops rather than by the animation.
* A loop rather than one-shot feedback, so none of the `--duration-*` tokens
* fit; the literal is one lap slow enough to read as travel, like the edge
* march above.
*/
.node-pulse.node-pulse-run {
animation: node-chase 1.2s linear infinite;
}
@keyframes node-chase {
to {
--node-pulse-angle: 1turn;
}
}
} }
/* Handles are neutral: the one brand-secondary affordance here is Run. */ /* Handles are neutral: the one brand-secondary affordance here is Run. */