From 6b70ba65fdd711b877cef3fd990b8033456a5761 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Mon, 1 Jun 2026 01:47:30 +0200 Subject: [PATCH] feat(editor): live step status on the graph during runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/src/editor_controller.dart | 25 +++++++++++++++++++++++++ lib/src/widgets/flow_canvas.dart | 20 +++++++++++++------- lib/src/widgets/run_tab.dart | 13 +++++++++++++ test/editor_controller_test.dart | 12 ++++++------ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/src/editor_controller.dart b/lib/src/editor_controller.dart index 778e51d..afc74ec 100644 --- a/lib/src/editor_controller.dart +++ b/lib/src/editor_controller.dart @@ -24,6 +24,12 @@ import 'model/auto_layout.dart'; import 'model/flow_graph.dart'; import 'model/layout_store.dart'; +/// Mirrors `FlowNodeStatus` from the flow_node widget, but +/// lives in the controller so the run tab can update it +/// without importing widgets. The canvas adapts between +/// the two. +enum StepRunStatus { idle, running, done, failed, awaiting } + class FlowEditorController extends ChangeNotifier { /// Active flow file name (without `.yaml`). `null` while /// the editor is empty. @@ -64,6 +70,14 @@ class FlowEditorController extends ChangeNotifier { bool get isRunning => _running; bool _running = false; + /// Live per-step status for the in-flight or most-recent + /// run. The canvas pulls colours and icons out of this map + /// so the graph view shows real-time progress even when + /// the operator triggered the run from the run tab. + Map get stepStatuses => + Map.unmodifiable(_stepStatuses); + final Map _stepStatuses = {}; + /// Flag flipped during graph -> YAML emission so we don't /// loop: the text change listener skips a reparse if it's /// already mid-emit. @@ -121,6 +135,17 @@ class FlowEditorController extends ChangeNotifier { set running(bool v) { if (_running == v) return; _running = v; + if (v) { + _stepStatuses.clear(); + } + notifyListeners(); + } + + /// Update the live status of one step. Called by the run + /// tab as events arrive from the host's FlowRunDriver. + void updateStepStatus(String stepId, StepRunStatus status) { + if (_stepStatuses[stepId] == status) return; + _stepStatuses[stepId] = status; notifyListeners(); } diff --git a/lib/src/widgets/flow_canvas.dart b/lib/src/widgets/flow_canvas.dart index cb34698..0ff032c 100644 --- a/lib/src/widgets/flow_canvas.dart +++ b/lib/src/widgets/flow_canvas.dart @@ -54,12 +54,7 @@ const double _inputsY = 80; class FlowCanvas extends StatefulWidget { final FlowEditorController controller; - final Map stepStatuses; - const FlowCanvas({ - super.key, - required this.controller, - this.stepStatuses = const {}, - }); + const FlowCanvas({super.key, required this.controller}); @override State createState() => _FlowCanvasState(); @@ -194,7 +189,8 @@ class _FlowCanvasState extends State { final pos = layout.positions[step.id]; if (pos == null) return const SizedBox.shrink(); final selected = widget.controller.selectedStepId == step.id; - final status = widget.stepStatuses[step.id] ?? FlowNodeStatus.idle; + final raw = widget.controller.stepStatuses[step.id] ?? StepRunStatus.idle; + final status = _toNodeStatus(raw); return Positioned( left: pos.x, top: pos.y, @@ -591,6 +587,16 @@ class _DropTarget { }); } +FlowNodeStatus _toNodeStatus(StepRunStatus s) { + return switch (s) { + StepRunStatus.idle => FlowNodeStatus.idle, + StepRunStatus.running => FlowNodeStatus.running, + StepRunStatus.done => FlowNodeStatus.done, + StepRunStatus.failed => FlowNodeStatus.failed, + StepRunStatus.awaiting => FlowNodeStatus.awaiting, + }; +} + class _DotGridPainter extends CustomPainter { final Color color; _DotGridPainter({required this.color}); diff --git a/lib/src/widgets/run_tab.dart b/lib/src/widgets/run_tab.dart index 73d34ad..82459ac 100644 --- a/lib/src/widgets/run_tab.dart +++ b/lib/src/widgets/run_tab.dart @@ -519,18 +519,31 @@ class _RunTabState extends State { switch (event) { case StepStarted(): _liveSteps[event.stepId] = const _StepState(kind: _StepKind.running); + widget.controller.updateStepStatus( + event.stepId, + StepRunStatus.running, + ); case StepCompleted(): _liveSteps[event.stepId] = _StepState( kind: _StepKind.done, durationMs: event.durationMs, ); + widget.controller.updateStepStatus(event.stepId, StepRunStatus.done); case StepFailed(): _liveSteps[event.stepId] = _StepState( kind: _StepKind.error, error: event.error, ); + widget.controller.updateStepStatus( + event.stepId, + StepRunStatus.failed, + ); case StepAwaitingApproval(): _liveSteps[event.stepId] = const _StepState(kind: _StepKind.awaiting); + widget.controller.updateStepStatus( + event.stepId, + StepRunStatus.awaiting, + ); } }); } diff --git a/test/editor_controller_test.dart b/test/editor_controller_test.dart index 3a6db19..ca4f7d0 100644 --- a/test/editor_controller_test.dart +++ b/test/editor_controller_test.dart @@ -43,9 +43,9 @@ steps: outputs: {} '''); final initialText = c.codeController.fullText; - c.applyGraphEdit(c.graph.withStepAdded( - const FlowStep(id: 'b', use: 'debug.echo@^0'), - )); + c.applyGraphEdit( + c.graph.withStepAdded(const FlowStep(id: 'b', use: 'debug.echo@^0')), + ); expect(c.codeController.fullText, isNot(initialText)); expect(c.codeController.fullText, contains('id: b')); expect(c.graph.steps, hasLength(2)); @@ -61,9 +61,9 @@ steps: outputs: {} '''); expect(c.isDirty, isFalse); - c.applyGraphEdit(c.graph.withStepAdded( - const FlowStep(id: 'b', use: 'debug.echo@^0'), - )); + c.applyGraphEdit( + c.graph.withStepAdded(const FlowStep(id: 'b', use: 'debug.echo@^0')), + ); expect(c.isDirty, isTrue); c.markSaved(); expect(c.isDirty, isFalse);