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

@ -7,10 +7,13 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'data/hub.dart';
import 'pages/approvals.dart';
import 'pages/audit.dart';
import 'pages/doctor.dart';
import 'pages/flows.dart';
import 'pages/modules.dart';
import 'theme/theme.dart';
import 'theme/tokens.dart';
@ -19,7 +22,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.7.3';
const String kStudioVersion = '0.8.0';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -121,6 +124,12 @@ class _StudioShellState extends State<StudioShell> {
selectedIcon: Icons.extension,
page: ModulesPage(),
),
_NavPage(
label: 'Flows',
icon: Icons.account_tree_outlined,
selectedIcon: Icons.account_tree,
page: FlowsPage(),
),
_NavPage(
label: 'Audit',
icon: Icons.timeline_outlined,
@ -160,22 +169,52 @@ class _StudioShellState extends State<StudioShell> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: Row(
children: [
_Sidebar(
selectedIndex: _selectedIndex,
onSelect: (i) => setState(() => _selectedIndex = i),
pages: _pages,
connected: _connected,
endpointLabel: HubService.instance.endpointLabel,
return Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
// Cmd+1..5 jumps to the matching destination. Numbered
// 1-based to match the visual order in the sidebar.
for (var i = 0; i < _pages.length; i++)
SingleActivator(
LogicalKeyboardKey(LogicalKeyboardKey.digit1.keyId + i),
meta: true,
): _GoToPageIntent(i),
// Cmd+, opens the settings dialog (macOS convention).
const SingleActivator(LogicalKeyboardKey.comma, meta: true):
const _OpenSettingsIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
_GoToPageIntent: CallbackAction<_GoToPageIntent>(
onInvoke: (intent) {
setState(() => _selectedIndex = intent.index);
return null;
},
),
Container(
width: 1,
color: theme.colorScheme.outlineVariant,
_OpenSettingsIntent: CallbackAction<_OpenSettingsIntent>(
onInvoke: (_) {
FaiSettingsDialog.show(context);
return null;
},
),
Expanded(
child: AnimatedSwitcher(
},
child: Focus(
autofocus: true,
child: Scaffold(
body: Row(
children: [
_Sidebar(
selectedIndex: _selectedIndex,
onSelect: (i) => setState(() => _selectedIndex = i),
pages: _pages,
connected: _connected,
endpointLabel: HubService.instance.endpointLabel,
),
Container(
width: 1,
color: theme.colorScheme.outlineVariant,
),
Expanded(
child: AnimatedSwitcher(
duration: FaiMotion.base,
switchInCurve: FaiMotion.easing,
switchOutCurve: FaiMotion.easing,
@ -189,18 +228,31 @@ class _StudioShellState extends State<StudioShell> {
child: child,
),
),
child: KeyedSubtree(
key: ValueKey(_selectedIndex),
child: _pages[_selectedIndex].page,
),
child: KeyedSubtree(
key: ValueKey(_selectedIndex),
child: _pages[_selectedIndex].page,
),
),
),
],
),
),
],
),
),
);
}
}
/// Intents driven by the Shortcuts/Actions pair on the shell.
class _GoToPageIntent extends Intent {
final int index;
const _GoToPageIntent(this.index);
}
class _OpenSettingsIntent extends Intent {
const _OpenSettingsIntent();
}
class _Sidebar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onSelect;