feat(flow editor): Format / Concat / IntToString / FloatToString palette nodes#207
Conversation
…tte nodes (#206) Surface the four built-in string reporter nodes in the flow editor so they can be authored (codegen shipped in flow-codegen#26). All four are `.other`-kind REPORTERS (rounded, data-only) producing a single `value` ([]const u8) output that wires into command data inputs (Log / Emit / SetVariable, …). They carry no exec pins — they sit off the exec spine. - flow_doc.isReporterTypeName: new type-name reporter classifier for the `.other` expression nodes (BinOp/Compare/Logic/Literal/Identifier/ GetComponent + the four string reporters). `nodeVisual` routes `.other` nodes through it so the string nodes get the rounded silhouette + reporter border. They're not `isControlNode`, so `needs_exec_in` stays false and they never get an exec-in anchor. - flow_doc render `.other` arms: - IntToString / FloatToString: one `value` data INPUT + one `value` data OUTPUT (same name, distinguished by pin direction in the id — matches codegen). - Concat / Format: a dynamic set of `arg<N>` data INPUT pins + one `value` data OUTPUT. The arg count is derived from the data edges already wired into the node plus one spare slot (`varArgInputCount`, mirroring `switchCaseOutputCount`), rendered from the static `var_arg_pin_names` table so `recordPin`'s borrowed names outlive the frame. - flow_doc palette: new "Text" cluster — + Format / + Concat / + IntToString / + FloatToString. Format seeds an empty `template` string literal (`""`, like Identifier's name seed). - flow_io.other_field_specs: Format exposes `template` (the std.fmt format string) as a `.text` field, edited + round-tripped as a quoted JSON string the same way Identifier.name is. - tests: appendOtherNode shape + seeded template, reporter classification (rounded, no exec-in), varArgInputCount / argIndexOf derivation, and parse/render round-trip (template persists, load -> save byte-stable).
PR SummaryLow Risk Overview Variadic args mirror Switch case counting: new Reporter styling: new public Exports Reviewed by Cursor Bugbot for commit 1997765. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Code Review
This pull request implements support for string and text reporter nodes (Format, Concat, IntToString, and FloatToString) in the flow editor. It introduces logic to dynamically calculate and render variadic input pins (arg<N>) based on active connections, styles these nodes as rounded reporters, and adds comprehensive unit tests. The review feedback suggests improving the robustness of pin name parsing in argIndexOf by strictly validating ASCII digits, and refactoring varArgInputCount to use @max for a more idiomatic Zig implementation.
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.
| pub fn argIndexOf(pin: []const u8) ?u32 { | ||
| if (!std.mem.startsWith(u8, pin, "arg")) return null; | ||
| const digits = pin[3..]; | ||
| if (digits.len == 0) return null; | ||
| return std.fmt.parseInt(u32, digits, 10) catch null; | ||
| } |
There was a problem hiding this comment.
The argIndexOf function parses the pin name suffix using std.fmt.parseInt. However, parseInt allows a leading + sign (e.g., arg+1 would parse as 1). To ensure strict correctness and prevent malformed pin names from being parsed as valid indices, we should verify that all characters in digits are ASCII digits before parsing.
pub fn argIndexOf(pin: []const u8) ?u32 {
if (!std.mem.startsWith(u8, pin, "arg")) return null;
const digits = pin[3..];
if (digits.len == 0) return null;
for (digits) |c| {
if (!std.ascii.isDigit(c)) return null;
}
return std.fmt.parseInt(u32, digits, 10) catch null;
}
There was a problem hiding this comment.
Done — argIndexOf now rejects any non-digit suffix before parseInt, so arg+1 (and other malformed forms) no longer parse. Matches caseIndexOf/codegen's isCaseExecPin.
| pub fn varArgInputCount(node_id: u32, edges: []const flow_io.Edge) u32 { | ||
| var highest: ?u32 = null; | ||
| for (edges) |e| { | ||
| if (e.to_node != node_id) continue; | ||
| const idx = argIndexOf(e.to_pin) orelse continue; | ||
| if (highest == null or idx > highest.?) highest = idx; | ||
| } |
There was a problem hiding this comment.
In Zig, we can write the tracking of the highest index more idiomatically by using @max and avoiding the force-unwrap highest.?.
pub fn varArgInputCount(node_id: u32, edges: []const flow_io.Edge) u32 {
var highest: ?u32 = null;
for (edges) |e| {
if (e.to_node != node_id) continue;
const idx = argIndexOf(e.to_pin) orelse continue;
highest = if (highest) |h| @max(h, idx) else idx;
}
There was a problem hiding this comment.
Done — highest = if (highest) |h| @max(h, idx) else idx;, dropping the force-unwrap.
There was a problem hiding this comment.
Pull request overview
Adds the four string/text formatting reporter nodes to the flow editor palette and rendering logic, aligning editor visuals/pins with the existing flow-codegen contract (issue #206) and bringing .other expression nodes in line with the intended “reporters are rounded” UI rule.
Changes:
- Adds a new “Text” palette section with
Format,Concat,IntToString, andFloatToStringnodes (including seedingFormat.template). - Implements data-pin rendering for the new nodes, including derived variadic
arg<N>inputs forConcat/FormatviavarArgInputCount. - Introduces reporter classification for
.otherexpression nodes (including the new string reporters) so they render with the rounded reporter silhouette; adds corresponding unit tests and round-trip coverage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/modules/flow_doc.zig |
Adds variadic arg-pin infrastructure, renders pins for new string reporter nodes, and classifies .other expression nodes as reporters for visuals. |
src/flow_io.zig |
Adds Format.template to other_field_specs so it’s editable via the inspector .text widget and round-trips correctly. |
src/tests.zig |
Adds tests covering creation defaults, reporter classification, variadic arg derivation, and parse/render round-tripping for the new nodes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// flow-codegen treats as pure-value reporters. The string/text reporters | ||
| /// (`Concat`/`Format`/`IntToString`/`FloatToString`, flow-codegen#26) join | ||
| /// this set so `nodeVisual` gives them the rounded shape and the generic | ||
| /// classifier never awards them an exec-in anchor (they're not | ||
| /// `isControlNode`, so `needs_exec_in` stays false). |
| // Load → save is byte-stable (these carry only string / no extras, | ||
| // so there's no numeric normalisation to settle). |
| test "string reporters classify as rounded reporters with no exec-in" { | ||
| // All four render as REPORTERS (rounded silhouette) — `nodeVisual` | ||
| // returns the reporter rounding (14) via `isReporterTypeName`, and | ||
| // they carry no exec pins so they're never swept onto the exec | ||
| // spine (`isReporterTypeName` set is disjoint from the control set). | ||
| const reporter_round: f32 = 14.0; |
argIndexOf rejects non-digit suffixes (parseInt accepted a leading '+'); varArgInputCount tracks the highest index via @max.
…) (#212) * feat(flow editor): input reporter palette nodes (labelle-gui#208) Surface the nine built-in input reporter nodes in the flow editor so they can be authored (codegen shipped in flow-codegen#51 + the parallel more-reporters PR). All are `.other`-kind REPORTERS (rounded, data-only) producing a single `value` output read inside per-frame flows — bool for the key/mouse predicates, f32 for the mouse getters. They carry no exec pins (they sit off the exec spine) and no data INPUT pins: the key/button is an on-node FIELD, not a pin. - IsKeyDown / IsKeyPressed / IsKeyReleased: a `key` field (a KeyboardKey tag, e.g. "space"), `value` output (bool). - IsMouseButtonDown / IsMouseButtonPressed / IsMouseButtonReleased: a `button` field (a MouseButton tag, e.g. "left"), `value` output (bool). - GetMouseX / GetMouseY / GetMouseWheel: no field, `value` output (f32). Mirrors the #207 string-reporter palette path: - flow_doc.isReporterTypeName: add the nine type names so nodeVisual rounds them and the generic classifier never awards them an exec-in anchor. - flow_doc render `.other` arms: a single shared arm emits one `value` data OUTPUT for all nine (no data inputs). - flow_io.other_field_specs: `key` (.text) for the three IsKey* nodes, `button` (.text) for the three IsMouseButton* nodes; GetMouse* need no spec. Seeds round-trip as quoted JSON strings codegen parses verbatim ({ "type": "IsKeyDown", "key": "space" }). - flow_doc palette: new "Input" cluster near the String cluster, seeding key/button defaults "space" / "left"; GetMouse* seed no extras. - tests: appendOtherNode shape + seeded field, reporter classification (rounded, no exec-in), parse/render round-trip (key/button persist, load -> save byte-stable). - gui_tests: bump the hidden test window to 1280x1080 so the grown node palette's bottom "Add raw call…" escape hatch isn't clipped below the inspector child's scroll fold (TE can't auto-scroll to an item ImGui culled), keeping palette_has_plugin_section's wildcard click resolvable. * refactor(palette): isInputReporter helper instead of a 9-way or chain (gemini #212)
Adds the four string-formatting reporter nodes (flow-codegen#26) to the flow editor palette. Closes #206. Follows the time-node palette work (#205).
What
+ Format(seeds an emptytemplate),+ Concat,+ IntToString,+ FloatToString.valueOUTPUT ([]const u8), no exec pins.IntToString/FloatToStringtake a singlevalueinput;Concat/Formattake VARIADICarg<N>inputs via a newvarArgInputCount(data-input analogue ofswitchCaseOutputCount— rendersarg0..arg<wired+1>, always one spare).templatefield —Formatgets an editabletemplate(.textwidget, same asIdentifier.name), round-tripping to{"type":"Format","template":"hits={d}"}.There was no per-type reporter classifier before: all
.otherbuilt-ins fell throughnodeVisual'selsearm to the rectangular command shape — so BinOp/Compare/Logic/Literal/Identifier/GetComponent rendered as commands despite being pure-value reporters, contradicting the RFC §6 intent the code states ("reporters rounded"). The newisReporterTypeNameset includes those existing expression nodes as well as the four new ones, so they now render rounded. This is deliberate: rounding only the new string reporters would leave the existing expression reporters rectangular — a worse inconsistency. Net effect: all pure-value expression nodes now share the rounded reporter silhouette; control nodes (Branch/loops/Switch/time gates) stay rectangular. Purely cosmetic, no functional/serialization change.Tests
zig build test→ 463/463 (+7 new);zig build gui-test→ 26/26;zig buildclean.Parent: #187.