feat(ui): Flows page (5th destination) + keyboard shortcuts

Two operator-DX additions:

1. **Flows** is the new third destination in the sidebar.
   Lists saved flows from the hub via the new HubAdmin
   ListFlows RPC. Each row has a Run button that opens an
   input dialog (key=value pairs, one per line, all values
   sent as text payloads — the binary-input case stays in
   `fai run` CLI). Flow runs in a non-dismissable progress
   dialog; output is shown per-key with monospace
   selectable text. Errors render with the exception detail
   for diagnosis.

2. **Keyboard shortcuts** at the shell level:
   - Cmd+1 / Cmd+2 / Cmd+3 / Cmd+4 / Cmd+5 jump to the
     matching destination (Doctor / Modules / Flows / Audit
     / Approvals).
   - Cmd+, opens the Settings dialog (macOS convention).

   Implemented via Flutter's Shortcuts/Actions/Intent triple
   so the bindings are discoverable in IDE devtools and
   composable with platform-specific overrides later.

Bumps fai_studio 0.7.3 -> 0.8.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-05 23:35:48 +02:00
parent 2f32023322
commit 753db4fc60
5 changed files with 508 additions and 24 deletions

View file

@ -106,6 +106,49 @@ class HubService {
);
}
/// Saved flows known to the hub.
Future<List<SavedFlow>> listFlows() async {
final flows = await _client.listFlows();
return flows
.map(
(f) => SavedFlow(
name: f.name,
path: f.path,
sizeBytes: f.sizeBytes.toInt(),
),
)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
}
/// Run a saved flow with the supplied text inputs. Returns
/// the named outputs as a map of UI-friendly strings.
Future<Map<String, String>> runSavedFlow({
required String name,
required Map<String, String> textInputs,
}) async {
final r = await _client.runSavedFlow(
name: name,
textInputs: textInputs,
);
return {
for (final entry in r.outputs.entries)
entry.key: _payloadToText(entry.value),
};
}
String _payloadToText(Payload p) {
if (p.hasText()) return p.text;
if (p.hasJson()) return p.json.toString();
if (p.hasBytes()) {
return '<bytes: ${p.bytes.data.length} bytes, ${p.bytes.mimeType}>';
}
if (p.hasFile()) {
return '<file: ${p.file.uri}>';
}
return '<unknown payload variant>';
}
Future<List<AuditEvent>> recentEvents({
int limit = 50,
List<String> types = const [],
@ -264,6 +307,18 @@ class ModuleSummary {
});
}
class SavedFlow {
final String name;
final String path;
final int sizeBytes;
const SavedFlow({
required this.name,
required this.path,
required this.sizeBytes,
});
}
class ModuleDetail {
final String name;
final String version;