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);