feat(studio): Flows page IS the editor — three-tab graph+text+run
Some checks failed
Security / Security check (push) Failing after 1s

Studio's Flows destination is now the swappable
fai_studio_flow_editor's full surface: a drag-and-drop
graph view, the raw YAML text editor, and the run tab
with inputs form + live step progress + outputs.

What's gone:
 - The old saved-flows list + per-row run dialog.
 - The separate "flow-editor" navigation destination (one
   icon, not two — operators reach the editor by clicking
   Flows, period).
 - _FlowEditorAdapter (no longer needed; FlowsPage hands
   the runDriver to the editor directly).

What's new:
 - lib/data/flow_run_driver.dart — StudioFlowRunDriver:
   adapter implementing the editor's FlowRunDriver. Maps
   runFlow() to HubService.runSavedFlow and events() to
   HubService.streamEvents filtered to step.* event types.
   Converts proto-shaped FlowOutput variants to the editor's
   host-agnostic FlowOutputValue family.
 - lib/pages/flows.dart — replaced with a 50-line wrapper
   that loads capabilities into the editor's picker dropdown
   and forwards the active locale.

Editor pinned to fai_studio_flow_editor 0.2.0 (commit
870cbc2). All editor state — file list, dirty tracking, tab
selection, graph layout sidecar, properties panel — lives in
the editor package now; Studio just supplies the hub bridge.

Version 0.51.10 -> 0.52.0 (minor bump — Flows-page contract
changed, separate flow-editor destination removed).

flutter analyze: 0 issues. flutter test: 12/12 green.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-01 00:52:23 +02:00
parent fe2ee0a323
commit 634fd52635
5 changed files with 179 additions and 1439 deletions

View file

@ -0,0 +1,112 @@
// 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:fai_studio_flow_editor/fai_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
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;
}
/// 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(''),
};
}
}

View file

@ -5,7 +5,6 @@
import 'dart:async';
import 'package:fai_studio_flow_editor/fai_studio_flow_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -27,7 +26,7 @@ import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the
/// running app self-identifies visible in the sidebar header
/// and quick-glance proof that you're seeing the current build.
const String kStudioVersion = '0.51.10';
const String kStudioVersion = '0.52.0';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -228,18 +227,17 @@ class _StudioShellState extends State<StudioShell> {
// the Store's detail sheet, and the Store with the
// "Installed" filter covers the listing role. Cmd+K still
// opens FaiModuleSheet for quick info.
// Flows destination IS the editor as of 0.52.0 the
// graph / text / run tabs all live behind one icon. The
// separate "flow-editor" destination is gone; the Cmd+K
// palette still navigates to flows when the operator
// wants the editor surface.
_NavPage(
id: 'flows',
icon: Icons.account_tree_outlined,
selectedIcon: Icons.account_tree,
page: FlowsPage(),
),
_NavPage(
id: 'flow-editor',
icon: Icons.code_outlined,
selectedIcon: Icons.code,
page: _FlowEditorAdapter(),
),
_NavPage(
id: 'audit',
icon: Icons.timeline_outlined,
@ -1322,22 +1320,7 @@ class _LanguageToggle extends StatelessWidget {
}
}
/// Adapter that resolves the swappable FlowEditorPage's
/// runtime dependencies (locale, hub onRun callback) from
/// Studio's BuildContext. The _NavPage destination list
/// holds widgets as const-ish, so the adapter sits in
/// between to grab live data.
class _FlowEditorAdapter extends StatelessWidget {
const _FlowEditorAdapter();
@override
Widget build(BuildContext context) {
final lang = Localizations.localeOf(context).languageCode;
return FlowEditorPage(
locale: lang == 'de' ? FlowEditorLocale.de : FlowEditorLocale.en,
onRun: (name) async =>
(await HubService.instance.runSavedFlow(name: name))
.cast<String, Object>(),
);
}
}
// _FlowEditorAdapter removed in 0.52.0 FlowsPage now hosts
// the editor directly with the full runDriver bridge. There
// is one Flows destination; the standalone editor route is
// gone.

File diff suppressed because it is too large Load diff

View file

@ -125,10 +125,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: daf6732893315c6841c4f32c69c19cd1dc3caca8
resolved-ref: "870cbc29f71a7fd9972755203714274ac128897b"
url: "https://git.flemming.ai/fai/studio-flow-editor"
source: git
version: "0.1.4"
version: "0.2.0"
fake_async:
dependency: transitive
description:

View file

@ -1,7 +1,7 @@
name: fai_studio
description: "F∆I Studio — desktop GUI for the F∆I hub"
publish_to: 'none'
version: 0.51.10
version: 0.52.0
environment:
sdk: ^3.11.0-200.1.beta