// StudioFlowRunDriver — adapter that bridges the editor // package's FlowRunDriver interface to Studio's HubService. // // Lives in Studio (not in the editor package) so the package // stays host-agnostic. The driver maps: // // - editor.runFlow(...) -> HubService.runSavedFlow // - editor.events() -> HubService.streamEvents filtered // to step.* events and reshaped // into the editor's FlowRunEvent // value classes. import 'dart:async'; import 'dart:typed_data'; import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart' as editor; import 'hub.dart'; class StudioFlowRunDriver implements editor.FlowRunDriver { StudioFlowRunDriver(); @override Future> runFlow({ required String flowName, required Map textInputs, required Map fileInputs, required Map fileMimes, }) async { final outputs = await HubService.instance.runSavedFlow( name: flowName, textInputs: textInputs, fileInputs: fileInputs, fileMimeTypes: fileMimes, ); final mapped = {}; for (final entry in outputs.entries) { mapped[entry.key] = _convertOutput(entry.value); } return mapped; } @override Future moduleInfo(String capability) async { try { final detail = await HubService.instance.moduleInfo(capability); return editor.ModuleSpec( capability: capability, inputs: detail.inputs .map( (f) => editor.ModuleField( name: f.name, type: f.type, description: f.description, ), ) .toList(), outputs: detail.outputs .map( (f) => editor.ModuleField( name: f.name, type: f.type, description: f.description, ), ) .toList(), ); } catch (_) { // Module not installed locally, or transient hub error. // Returning null lets the editor fall back to YAML- // derived ports instead of failing the whole canvas. return null; } } @override Stream events() { return HubService.instance .streamEvents( backfill: 0, types: const [ 'step.started', 'step.completed', 'step.failed', 'step.awaiting_approval', 'step.approved', 'step.rejected', ], ) .map(_convertEvent) .where((e) => e != null) .cast(); } editor.FlowRunEvent? _convertEvent(AuditEvent e) { final flowName = e.flowName; final stepId = e.stepId; if (flowName == null || flowName.isEmpty) return null; if (stepId == null || stepId.isEmpty) return null; switch (e.type) { case 'step.started': case 'step.approved': return editor.StepStarted(flowName: flowName, stepId: stepId); case 'step.completed': return editor.StepCompleted( flowName: flowName, stepId: stepId, durationMs: e.durationMs ?? 0, ); case 'step.failed': case 'step.rejected': return editor.StepFailed( flowName: flowName, stepId: stepId, error: e.error ?? '', ); case 'step.awaiting_approval': return editor.StepAwaitingApproval(flowName: flowName, stepId: stepId); } return null; } @override Future pendingApprovalIdForStep({ required String flowName, required String stepId, }) async { try { final pending = await HubService.instance.pendingApprovals(); // Match the newest approval for this (flow, step). The hub // creates one approval row per step invocation; if multiple // are pending we surface the most recent (operator can still // navigate to the standalone Approvals page for the others). final match = pending .where((p) => p.flowName == flowName && p.stepId == stepId) .toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return match.isEmpty ? null : match.first.id; } catch (_) { return null; } } @override Future approveApproval({ required String approvalId, required String reviewer, }) => HubService.instance.approve(approvalId, reviewer); @override Future rejectApproval({ required String approvalId, required String reviewer, required String reason, }) => HubService.instance.reject(approvalId, reviewer, reason); /// Studio's FlowOutput hierarchy is gRPC-shaped (one class /// per payload variant); the editor's is host-agnostic /// (text / json / bytes only). File outputs degrade to /// JSON containing the URI string — the editor's run-tab /// renderer is read-only so the operator can still see /// where the file went; Studio's richer FaiFlowOutput /// widget keeps file-open affordances available elsewhere. editor.FlowOutputValue _convertOutput(FlowOutput out) { return switch (out) { FlowOutputText() => editor.FlowOutputText(out.text), FlowOutputJson() => editor.FlowOutputJson(out.pretty), FlowOutputBytes() => editor.FlowOutputBytes(out.bytes, out.mimeType), FlowOutputFile() => editor.FlowOutputJson({ 'file_uri': out.uri, 'mime_type': out.mimeType, }), FlowOutputUnknown() => const editor.FlowOutputText(''), }; } }