Skip to content

feat(flow editor): Once / Cooldown / Delay palette nodes#205

Merged
apotema merged 2 commits into
mainfrom
feat/palette-time-nodes
Jun 8, 2026
Merged

feat(flow editor): Once / Cooldown / Delay palette nodes#205
apotema merged 2 commits into
mainfrom
feat/palette-time-nodes

Conversation

@apotema

@apotema apotema commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Adds the three time-control built-in nodes to the flow editor palette so they're usable in the editor (codegen shipped in flow-codegen#47/#48).

What

  • Palette — a new "Time" cluster: + Once, + Cooldown / + Delay (seeding seconds = 1.0).
  • Exec pins — all three are command nodes with an exec-in and a single body exec-out, via controlExecPinNames (which isControlNode/execPinValidFor/validateExecEdge all derive from). New render arm draws the body out.
  • seconds fieldCooldown/Delay expose an editable seconds via the generic .other-field path (.literal widget, same as Literal.value); round-trips to .flow.jsonc matching the codegen contract ({"type":"Cooldown","seconds":1.0}). Once has no fields.

Latent bug fixed along the way

needs_exec_in only covered isCommandNode (true for plugin/CustomNodes), but the built-in control nodes (Branch/ForRange/While/Switch and now the time nodes) are .other-kind → isCommandNode was false, so a freshly-placed control node had no exec-in anchor until something already targeted it. Now gated on isControlNode too — fixes the new nodes and that pre-existing gap.

Tests

zig build test456/456; zig build gui-test26/26; zig build clean. New tests: add each node (type + seeded seconds), exec-pin validity returns body, and save/load round-trip preserving seconds.

Parent: #187. Pairs with the demo ticket #204.

Surface the three time-control command nodes in the flow editor so they
can be authored (codegen for them shipped in flow-codegen#47/#48). All
three are `.other`-kind command nodes with a single `body` exec output,
matching ForRange/While's exec-pin shape and rectangular silhouette.

- flow_doc.controlExecPinNames: Once/Cooldown/Delay -> {"body"}, which
  flows into isControlNode / execPinValidFor / validateExecEdge.
- flow_doc render: new `.other` arm draws the `body` exec-out; extend
  needs_exec_in to cover any control node so a freshly-placed node always
  exposes its exec-in drop target (else the first control arrow can't
  land — also fixes Branch/ForRange/While/Switch on first placement).
- flow_doc palette: new "Time" cluster — + Once, + Cooldown, + Delay.
  Cooldown/Delay seed `seconds` = 1.0.
- flow_io.other_field_specs: Cooldown/Delay expose `seconds` as a
  `.literal` inspector field so the f64 round-trips to .flow.jsonc.
- tests: appendOtherNode shape + seeded seconds, body exec-edge validity
  for each, and parse/render round-trip (1.0 normalises to 1, then load
  -> save is byte-stable).
@cursor

cursor Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Editor-only palette, pin, and inspector wiring with a localized exec-in gating fix; no auth, persistence, or codegen changes in this diff.

Overview
Time-control nodes — New Time palette section with + Once, + Cooldown, and + Delay. Cooldown/Delay seed seconds (palette uses "1", normalized like other literals). All three get exec-in plus a single body exec-out (same shape as loops); controlExecPinNames and rendering treat them like other control nodes. Cooldown/Delay use the .literal inspector path via other_field_specs.

Exec-in fixneeds_exec_in now includes isControlNode, so built-in control nodes (Branch, loops, Switch, and the new time nodes) show an exec-in anchor when first placed, not only after something already targets them.

Tests — Coverage for creation, body exec validation, and save/load round-trip for seconds.

Reviewed by Cursor Bugbot for commit 266223e. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for three new time-control command nodes (Once, Cooldown, and Delay) to the flow editor, including their field specifications, execution pin definitions, rendering logic, palette entries, and corresponding tests. The review feedback suggests grouping the execution pin checks for these nodes into a single conditional block to reduce duplication, seeding the default seconds value as "1" instead of "1.0" to avoid a one-time normalization diff on save, and updating related comments to reflect this normalized value.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/flow_io.zig
Comment on lines +354 to +360
// Time-control command nodes (flow-codegen#47, #48). `Cooldown` and
// `Delay` each carry one `seconds` field (a JSON-native `f64` literal,
// e.g. `1.0`) edited verbatim through the `.literal` widget so it
// normalises + round-trips like any other numeric extra. `Once` has no
// field, so it needs no spec — its inspector shows the verbatim view.
.{ .type_name = "Cooldown", .key = "seconds", .label = "Seconds", .widget = .literal },
.{ .type_name = "Delay", .key = "seconds", .label = "Seconds", .widget = .literal },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the editor's numeric canonical form normalizes 1.0 to 1 on first parse/save, updating the comment to use 1 instead of 1.0 keeps the documentation aligned with the actual serialized output.

    // Time-control command nodes (flow-codegen#47, #48). `Cooldown` and
    // `Delay` each carry one `seconds` field (a JSON-native `f64` literal,
    // e.g. `1`) edited verbatim through the `.literal` widget so it
    // normalises + round-trips like any other numeric extra. `Once` has no
    // field, so it needs no spec — its inspector shows the verbatim view.
    .{ .type_name = "Cooldown", .key = "seconds", .label = "Seconds", .widget = .literal },
    .{ .type_name = "Delay", .key = "seconds", .label = "Seconds", .widget = .literal }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed at the source: the seed is now "1", so the value is canonical from creation and there's no one-time 1.01 churn on first load.

Comment thread src/modules/flow_doc.zig
Comment on lines +734 to +736
if (std.mem.eql(u8, type_name, "Once")) return &.{"body"};
if (std.mem.eql(u8, type_name, "Cooldown")) return &.{"body"};
if (std.mem.eql(u8, type_name, "Delay")) return &.{"body"};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These three time-control nodes all return the same body exec pin. We can group their checks into a single if statement to reduce duplication and improve readability.

    if (std.mem.eql(u8, type_name, "Once") or
        std.mem.eql(u8, type_name, "Cooldown") or
        std.mem.eql(u8, type_name, "Delay")) return &.{"body"};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as separate if lines — it matches the existing convention in this function (ForRange/While are already one-per-line), which keeps each node type greppable. The duplication is one short line each; consolidating only the body-returners would read as inconsistent with the Branch/Switch arms around them.

Comment thread src/modules/flow_doc.zig
Comment on lines +2931 to +2947
// time; `Cooldown`/`Delay` carry a `seconds` (f64) field seeded to
// `1.0` — the same canonical JSON value text codegen parses
// (`{ "type": "Cooldown", "seconds": 1.0 }`). Like the other control
// nodes their wiring is the `body` exec edge; `Delay`'s `body` is meant
// to reach a `Subflow` (enforced by codegen, not the editor).
zgui.text("Time", .{});
if (zgui.button("+ Once", .{})) {
addOtherNode(s, "Once", &.{}) catch |err| nodeAddErr(err);
}
zgui.sameLine(.{});
if (zgui.button("+ Cooldown", .{})) {
addOtherNode(s, "Cooldown", &.{.{ .key = "seconds", .value_text = "1.0" }}) catch |err| nodeAddErr(err);
}
zgui.sameLine(.{});
if (zgui.button("+ Delay", .{})) {
addOtherNode(s, "Delay", &.{.{ .key = "seconds", .value_text = "1.0" }}) catch |err| nodeAddErr(err);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Seeding the default value as "1" instead of "1.0" avoids a one-time normalization diff on the first save of a newly placed node, keeping the file immediately stable and clean.

    // time; `Cooldown`/`Delay` carry a `seconds` (f64) field seeded to
    // `1` — the same canonical JSON value text codegen parses
    // (`{ "type": "Cooldown", "seconds": 1 }`). Like the other control
    // nodes their wiring is the `body` exec edge; `Delay`'s `body` is meant
    // to reach a `Subflow` (enforced by codegen, not the editor).
    zgui.text("Time", .{});
    if (zgui.button("+ Once", .{})) {
        addOtherNode(s, "Once", &.{}) catch |err| nodeAddErr(err);
    }
    zgui.sameLine(.{});
    if (zgui.button("+ Cooldown", .{})) {
        addOtherNode(s, "Cooldown", &.{.{ .key = "seconds", .value_text = "1" }}) catch |err| nodeAddErr(err);
    }
    zgui.sameLine(.{});
    if (zgui.button("+ Delay", .{})) {
        addOtherNode(s, "Delay", &.{.{ .key = "seconds", .value_text = "1" }}) catch |err| nodeAddErr(err);
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — seeding "1" (already the canonical f64 form) for both Cooldown and Delay, so there's no first-parse normalization.

Seeding "1" (already the editor's canonical f64 form) instead of "1.0"
avoids a one-time parse-normalization on first load. JSON-native f64, so
flow-codegen parses it identically.
@apotema apotema merged commit da8f9d4 into main Jun 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant