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
+17
View File
@@ -18,6 +18,9 @@ type Listener = () => void
const values = new Map<string, LiveValue>()
const statuses = new Map<string, LiveStatus>()
// How many times a node has emitted. The number itself means nothing; a change
// is what restarts the pulse.
const emits = new Map<string, number>()
const listeners = new Map<string, Set<Listener>>()
let connected = false
@@ -72,6 +75,10 @@ export const liveStore = {
getStatus(nodeId: string) {
return statuses.get(nodeId)
},
recordEmit(nodeId: string) {
emits.set(nodeId, (emits.get(nodeId) ?? 0) + 1)
notify(`emit:${nodeId}`)
},
setConnected(next: boolean) {
if (connected === next) return
connected = next
@@ -85,6 +92,8 @@ export const liveStore = {
values.clear()
for (const key of statuses.keys()) notify(`status:${key}`)
statuses.clear()
for (const key of emits.keys()) notify(`emit:${key}`)
emits.clear()
},
}
@@ -102,6 +111,14 @@ export function useNodeStatus(nodeId: string): LiveStatus | undefined {
)
}
/** Increments each time the node publishes something. */
export function useNodeEmits(nodeId: string): number {
return useSyncExternalStore(
(listener) => subscribeKey(`emit:${nodeId}`, listener),
() => emits.get(nodeId) ?? 0,
)
}
export function useLiveConnection(): boolean {
return useSyncExternalStore(
(listener) => {