chain-studio/lib/data/flow_run_driver.dart
flemming-it 891acd2ba2
Some checks failed
Security / Security check (push) Failing after 2s
refactor: rename internal Fai* design system + fai_ helpers to chain
The Studio design system, widgets and helpers carried a Fai* / fai_
prefix (FaiSpace, FaiColors, FaiTheme, FaiLog, 17 fai_*.dart files, the
faiBinary* l10n keys). Studio is the Ch∆In product, so rename them to
Chain* / chain_ — carefully preserving English fail/failure/failed.
Also fix stale references: the 'fai' binary in l10n strings -> 'chain',
FAI_* env vars (FAI_BIN/DATA_DIR/MODULES_DIR/TODAY/BOOTSTRAP_TOKEN) ->
CHAIN_*, fai_platform -> fai_chain, fai_hub -> chain_hub. Vendor
security-hook tooling (FAI_BANNED_TERMS_FILE) + the .fai bundle ext left.
flutter analyze + test: clean (20 passed).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-06-16 17:53:17 +02:00

178 lines
5.6 KiB
Dart

// 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<Map<String, editor.FlowOutputValue>> runFlow({
required String flowName,
required Map<String, String> textInputs,
required Map<String, Uint8List> fileInputs,
required Map<String, String> fileMimes,
}) async {
final outputs = await HubService.instance.runSavedFlow(
name: flowName,
textInputs: textInputs,
fileInputs: fileInputs,
fileMimeTypes: fileMimes,
);
final mapped = <String, editor.FlowOutputValue>{};
for (final entry in outputs.entries) {
mapped[entry.key] = _convertOutput(entry.value);
}
return mapped;
}
@override
Future<editor.ModuleSpec?> 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<editor.FlowRunEvent> 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>();
}
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<String?> 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<void> approveApproval({
required String approvalId,
required String reviewer,
}) =>
HubService.instance.approve(approvalId, reviewer);
@override
Future<void> 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 ChainFlowOutput
/// 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(''),
};
}
}