chain-studio-flow-editor/lib/src/widgets/capability_picker.dart
flemming-it 870cbc29f7 feat(editor): three-tab WYSIWYG editor — graph / text / run
Full rewrite of the editor surface, layered on top of the
FlowGraph foundation. One in-memory flow drives three tabs
that the operator can flip between freely:

 - Graph: a drag-and-drop canvas. Nodes are step cards with
   port dots on their left (one per `with:` field) and a
   combined output port on the right. Pinned inputs and
   outputs pseudo-nodes sit at the left and right edges so
   every flow has a visually obvious source and sink. Pan +
   zoom via InteractiveViewer; drag a node by its body to
   reposition it (positions persisted to a sidecar JSON file
   under ~/.fai/data/flows/.layout/<name>.json — kept OUT of
   the YAML so `fai run` stays byte-stable).
 - Text: the existing YAML CodeField with expands:true so
   line 1 anchors at the top edge. YAML-aware syntax
   highlighting picks up the theme's primary / secondary /
   tertiary palette for keys / strings / numbers.
 - Run: an inputs form (text fields + file-pick), a Start
   button that calls the host's FlowRunDriver, a live step
   list driven by the driver's event stream (matches the
   `fai run` CLI rendering — ◻ pending, · running, ✔ done +
   duration, ✗ failed, ⏸ awaiting approval), and the typed
   outputs once the run resolves.

Source of truth = the YAML text. Graph edits emit fresh YAML
into the shared CodeController; text edits re-parse the
graph on a 350 ms debounce. Layout sidecar persists drag
positions only.

New public API (lib/fai_studio_flow_editor.dart):

  FlowEditorPage(
    initialFlowName: ...,
    locale: ...,
    runDriver: FlowRunDriver?,        // NEW — host bridge
    availableCapabilities: List<String>, // NEW — for the
                                          // capability picker
                                          // dialog when adding
                                          // a step
  )

The host (Studio) implements FlowRunDriver to bridge the
hub's gRPC SDK into the editor's event vocabulary. The
StepStarted/Completed/Failed/AwaitingApproval events are
shared verbatim with the CLI's run_progress renderer so
both surfaces speak the same visual language.

Files in this commit:
 - lib/src/editor_controller.dart       — shared state +
   debounced reparse loop
 - lib/src/run_driver.dart              — host bridge
   interface + event types
 - lib/src/widgets/flow_canvas.dart     — pan / zoom / drag /
   port-to-port connection drawing
 - lib/src/widgets/flow_node.dart       — node card primitive
   (module / approval / inputs / outputs variants)
 - lib/src/widgets/edge_painter.dart    — single CustomPainter
   for every edge + draft drag line, cubic bezier with
   arrow-head caps
 - lib/src/widgets/properties_panel.dart — right-side editor
   when a step is selected (rename id, change capability, add
   / remove / rename with-fields, delete step)
 - lib/src/widgets/capability_picker.dart — searchable list
   dialog used by Add-step
 - lib/src/widgets/run_tab.dart         — inputs form +
   live step progress + outputs renderer
 - lib/src/flow_editor_page.dart        — host scaffolding,
   toolbar, file list, three-tab body, keyboard shortcuts
 - lib/src/l10n.dart                    — EN + DE strings for
   every new label
 - lib/fai_studio_flow_editor.dart      — exports the new
   public types (FlowRunDriver, FlowRunEvent variants,
   FlowOutputValue variants)

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

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-01 00:48:35 +02:00

103 lines
3.2 KiB
Dart

// Capability picker — modal dialog for choosing which
// capability a new step should use. Studio provides the
// list (it lives on the hub); the picker is a thin
// searchable list on top.
import 'package:flutter/material.dart';
import '../l10n.dart';
import '../tokens.dart';
class CapabilityPicker extends StatefulWidget {
final List<String> capabilities;
final FlowEditorStrings strings;
const CapabilityPicker({
super.key,
required this.capabilities,
required this.strings,
});
static Future<String?> show(
BuildContext context, {
required List<String> capabilities,
required FlowEditorStrings strings,
}) {
return showDialog<String>(
context: context,
builder: (_) =>
CapabilityPicker(capabilities: capabilities, strings: strings),
);
}
@override
State<CapabilityPicker> createState() => _CapabilityPickerState();
}
class _CapabilityPickerState extends State<CapabilityPicker> {
String _query = '';
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final filtered = _query.isEmpty
? widget.capabilities
: widget.capabilities
.where((c) => c.toLowerCase().contains(_query.toLowerCase()))
.toList();
return Dialog(
child: SizedBox(
width: 480,
height: 520,
child: Padding(
padding: const EdgeInsets.all(FaiSpace.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
widget.strings.pickerTitle,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: FaiSpace.sm),
TextField(
autofocus: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search, size: 18),
hintText: widget.strings.pickerSearch,
isDense: true,
border: const OutlineInputBorder(),
),
onChanged: (v) => setState(() => _query = v),
),
const SizedBox(height: FaiSpace.sm),
Expanded(
child: filtered.isEmpty
? Center(
child: Text(
widget.strings.pickerEmpty,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
)
: ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) {
final cap = filtered[i];
return ListTile(
dense: true,
title: Text(
cap,
style: const TextStyle(fontFamily: 'monospace'),
),
onTap: () => Navigator.pop(context, cap),
);
},
),
),
],
),
),
),
);
}
}