The 0.2.x canvas auto-recomputed the outputs endpoint's X
position from `max(step.x) + nodeWidth + 120` every build.
Dragging any step rebuilt the layout, the recompute kicked
in, and the outputs panel + every edge entering it
shifted across the screen. Mirror image: the inputs panel
was nailed to a hardcoded (40, 80) but the right side of
the world moved every time the operator touched a node.
Visually catastrophic; Stefan flagged it as the same kind
of "everything moves when I touch one thing" failure that
shelved the jai_client editor.
Structural fix: treat inputs and outputs as first-class
nodes with positions stored in the same layout sidecar as
every step. There is no more auto-recompute path.
Concretely:
- AutoLayout.layout now seeds NodePositions for the
reserved IDs `__inputs__` (column −1) and `__outputs__`
(column max+1) on first open. Existing positions are
never overwritten so subsequent layout passes don't
fight operator-chosen placements.
- FlowCanvas reads inputs/outputs positions from
layout.positions instead of hardcoded `_inputsX` /
`_inputsY` constants and the computed `_outputsX(...)`.
Both functions are deleted; the canvas now has one
single source of truth for ALL node positions, the
sidecar.
- The inputs / outputs endpoints render via a new
`_endpointPositioned` helper that mirrors
`_stepPositioned` — same FlowNode widget, same
drag handler, same `controller.moveStep` path. The
operator can grab the inputs panel and slide it
wherever; the position persists to the sidecar like
every step.
- All port-position helpers (`_outputPortPosition`,
`_inputPortPosition`, the inputs-endpoint port
position) now take a single `FlowLayout` and read
coordinates from there. No more `double outputsX`
parameter threaded through every method.
- Fit-to-content now expands the bounding box using the
endpoints' SIDECAR positions rather than a re-derived
outputs X. Same single source of truth.
Side-benefit: the editor's coordinate model is now strictly
sidecar-driven. No state derivation lives in render code.
This rules out the catastrophic "drag-mid-flight-redoes-
coordinate-transforms" failure mode jai_client hit (where
internal offsets grew during a drag and broke port
positions).
flutter analyze clean, 12/12 tests pass.
Signed-off-by: flemming-it <sf@flemming.it>
While the operator is dragging from an output port, every
input port now lights up with a tint so they can see at a
glance where the connection can land. The closest port
within the snap distance gets a larger halo + brighter
border so the operator knows which port will accept the
drop if they release now.
Visual states:
- input port, no drag: muted background
- input port, drag in flight: primary @ 35% alpha
- input port, closest target: primary fill + 2.5 px border
+ soft halo glow
The closest-target computation runs once per port per
frame during a drag — O(n) over visible input ports.
Negligible at flow sizes operators care about; revisit if
flows exceed a few dozen steps.
Signed-off-by: flemming-it <sf@flemming.it>
Two polishing tweaks on the graph tab:
- When the active flow has no steps, the canvas would have
rendered as a near-empty grid (just inputs / outputs
sidebars). Overlay a centred call-to-action with the
graph icon, the "No steps yet" / "Noch keine Schritte"
heading, a one-liner explainer, and a primary Add Step
button that opens the capability picker directly.
- Auto-fit zoom on first open of each flow + a floating
"Fit to screen" button (bottom-right corner of the
canvas viewport). The auto-fit re-runs only when the
flow name changes, so the operator's manual pan / zoom
on the current flow is preserved. The fit math expands
the bounding box to include inputs + outputs pseudo-
nodes too, so wide flows zoom out enough to show every
port at once.
Fit math:
- Walk every step's stored position + height; merge with
the inputs sidebar's known position.
- Compute scale that makes the bounding box fit the
viewport with 80 px padding on each side.
- Clamp to [0.4, 2.0] — InteractiveViewer's own bounds.
- Translate so the box centres in the viewport.
`flutter analyze` clean, all 11 tests still pass.
Signed-off-by: flemming-it <sf@flemming.it>
Step run events were previously visible only on the run tab.
Operators who triggered a run there and then switched to
the graph tab saw a static graph — the canvas had no idea
something was executing.
Wire the run events through the FlowEditorController so all
three tabs share the same status snapshot:
- New StepRunStatus enum on the controller
(idle / running / done / failed / awaiting).
- controller.stepStatuses exposes the live map.
- controller.updateStepStatus is called from the run tab's
event handler — one source of truth, both views read it.
- FlowCanvas drops its own stepStatuses parameter and
pulls from the controller instead, so the canvas now
shows the running pulse + done check + error cross + pause
icon in each step's header live, in lockstep with the
step list on the run tab.
When a fresh run starts (controller.running = true) the
status map is cleared so stale events from a previous run
don't paint the new one.
Signed-off-by: flemming-it <sf@flemming.it>
Full rewrite of the editor surface, layered on top of the
FlowGraph foundation. One in-memory flow drives three tabs
that the operator can flip between freely:
- Graph: a drag-and-drop canvas. Nodes are step cards with
port dots on their left (one per `with:` field) and a
combined output port on the right. Pinned inputs and
outputs pseudo-nodes sit at the left and right edges so
every flow has a visually obvious source and sink. Pan +
zoom via InteractiveViewer; drag a node by its body to
reposition it (positions persisted to a sidecar JSON file
under ~/.fai/data/flows/.layout/<name>.json — kept OUT of
the YAML so `fai run` stays byte-stable).
- Text: the existing YAML CodeField with expands:true so
line 1 anchors at the top edge. YAML-aware syntax
highlighting picks up the theme's primary / secondary /
tertiary palette for keys / strings / numbers.
- Run: an inputs form (text fields + file-pick), a Start
button that calls the host's FlowRunDriver, a live step
list driven by the driver's event stream (matches the
`fai run` CLI rendering — ◻ pending, · running, ✔ done +
duration, ✗ failed, ⏸ awaiting approval), and the typed
outputs once the run resolves.
Source of truth = the YAML text. Graph edits emit fresh YAML
into the shared CodeController; text edits re-parse the
graph on a 350 ms debounce. Layout sidecar persists drag
positions only.
New public API (lib/fai_studio_flow_editor.dart):
FlowEditorPage(
initialFlowName: ...,
locale: ...,
runDriver: FlowRunDriver?, // NEW — host bridge
availableCapabilities: List<String>, // NEW — for the
// capability picker
// dialog when adding
// a step
)
The host (Studio) implements FlowRunDriver to bridge the
hub's gRPC SDK into the editor's event vocabulary. The
StepStarted/Completed/Failed/AwaitingApproval events are
shared verbatim with the CLI's run_progress renderer so
both surfaces speak the same visual language.
Files in this commit:
- lib/src/editor_controller.dart — shared state +
debounced reparse loop
- lib/src/run_driver.dart — host bridge
interface + event types
- lib/src/widgets/flow_canvas.dart — pan / zoom / drag /
port-to-port connection drawing
- lib/src/widgets/flow_node.dart — node card primitive
(module / approval / inputs / outputs variants)
- lib/src/widgets/edge_painter.dart — single CustomPainter
for every edge + draft drag line, cubic bezier with
arrow-head caps
- lib/src/widgets/properties_panel.dart — right-side editor
when a step is selected (rename id, change capability, add
/ remove / rename with-fields, delete step)
- lib/src/widgets/capability_picker.dart — searchable list
dialog used by Add-step
- lib/src/widgets/run_tab.dart — inputs form +
live step progress + outputs renderer
- lib/src/flow_editor_page.dart — host scaffolding,
toolbar, file list, three-tab body, keyboard shortcuts
- lib/src/l10n.dart — EN + DE strings for
every new label
- lib/fai_studio_flow_editor.dart — exports the new
public types (FlowRunDriver, FlowRunEvent variants,
FlowOutputValue variants)
flutter analyze: 0 issues. flutter test: 7/7 green.
Signed-off-by: flemming-it <sf@flemming.it>