Header of process and node ports follow each other both ways: a panel edit rewrites only the def line of edited code, a code save reads the header back into input ports and settings (draft and response); Space types in the node editor again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 18:19:19 +02:00
co-authored by Claude Opus 5.5
parent 92c562a7d5
commit 762e7fb1fe
15 changed files with 645 additions and 48 deletions
+24 -34
View File
@@ -49,6 +49,13 @@ import useCustomToast from "@/hooks/useCustomToast"
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
import { cn } from "@/lib/utils"
import { DtypeValue } from "./FlowBoundary"
import {
IDENTIFIER,
portName,
processArgs,
RESERVED_PARAMS,
withHeader,
} from "./header"
import { JsonField } from "./JsonField"
import { MessageSparkline } from "./MessageSparkline"
import {
@@ -92,12 +99,6 @@ const NO_SECRET = "__none__"
const SECTION = PANEL_SECTION
/** Settings the engine reads itself, so they are not the author's to name. */
const RESERVED_PARAMS = new Set(["synchronous"])
/** A setting reaches the function by name, so the name has to be one. */
const IDENTIFIER = /^[A-Za-z_]\w*$/
/** Sentinels for the resources dropdown, which has two options that are not sizes. */
const NO_RESOURCES = "__shared__"
const CUSTOM_RESOURCES = "__custom__"
@@ -582,6 +583,8 @@ function FreeParamsForm({
// remount its row and take the half-typed value with it.
<div key={`param-${index}`} className="flex items-center gap-1.5">
<Input
// Its own key, so a name the code's header changed shows up.
key={key}
defaultValue={key}
placeholder="name"
aria-label="Setting name"
@@ -1364,10 +1367,6 @@ function StoredStateSection({ nodeType }: { nodeType: string }) {
)
}
/** The name the node function sees; the engine derives it the same way. */
const portName = (spec: MessageSpec) =>
spec.port || (spec.name ?? "").split(".").pop() || ""
/**
* Whether a port was named by hand rather than after its message. Nothing
* stores that: a port whose name is not the one its message gives it is one.
@@ -1376,25 +1375,11 @@ const ownsName = (spec: MessageSpec) =>
Boolean(spec.port) && spec.port !== portName({ name: spec.name })
/**
* A `process` that takes this node's inputs and settings, and returns its
* outputs.
*
* Both arrive by name, so both are arguments — the ports first, in the order
* they are declared, then the settings.
* A `process` that takes this node's inputs and settings (`processArgs`), and
* returns its outputs.
*/
function scaffoldFor(node: NodeDef_Input): string {
const args = [
...new Set(
[
...(node.requires ?? []).map(portName),
...Object.keys(node.params ?? {}).filter(
(name) => !RESERVED_PARAMS.has(name),
),
]
// Anything else cannot be a keyword argument, so it cannot be one.
.filter((name) => IDENTIFIER.test(name)),
),
]
const args = processArgs(node)
const returns = (node.provides ?? [])
.filter((spec) => portName(spec))
.map((spec) => `"${portName(spec)}": ${PLACEHOLDER[spec.dtype ?? "float"]}`)
@@ -1470,11 +1455,12 @@ function PanelBody({
}
/**
* Change the node, and keep an untouched scaffold in step with its ports.
* Change the node, and keep its code in step with its ports and settings.
*
* Only code that is still exactly what this panel generates is rewritten —
* one edit of your own and it is yours, ports or no ports. Shared code is
* never rewritten either: the other flows using it have their own ports.
* Code that is still exactly what this panel generates is generated again,
* outputs included. Code of your own only has the header of `process`
* rewritten, and the body is left as it is. Shared code is never rewritten:
* the other flows using it have their own ports.
*/
const editNode = (next: NodeDef_Input) => {
onChange(next)
@@ -1487,8 +1473,10 @@ function PanelBody({
current === undefined
)
return
const wanted = scaffoldFor(next)
if (current !== wanted && SCAFFOLD_SHAPE.test(current)) editCode(wanted)
const wanted = SCAFFOLD_SHAPE.test(current)
? scaffoldFor(next)
: withHeader(current, processArgs(next))
if (current !== wanted) editCode(wanted)
}
/**
@@ -1705,7 +1693,9 @@ function PanelBody({
{CODE_LOCKED}
</p>
)}
<div className="min-h-0 flex-1 overflow-hidden rounded-md border border-border">
{/* `nokey`: Monaco types through a plain div (EditContext), which
the canvas takes for itself, so Space would pan instead. */}
<div className="nokey min-h-0 flex-1 overflow-hidden rounded-md border border-border">
<Suspense
fallback={
<div className="h-full w-full animate-pulse bg-muted" />