feat(flows): sealed-aware flow list — hub sample flag, own dir, sample import
Three connection-truth fixes on the flow surface: * The editor now lists the CONNECTED hub's flows: inside a sealed area Studio passes the instance's own flows dir (~/.chain/sealed/<slug>/data/flows) — previously the editor kept showing the shared hub's files whatever the connection, so a sealed area's list was simply wrong (and runSavedFlow hit the other hub's namespace). * A connection switch replaces the editor state entirely (keyed by the sealed slug): an open buffer from one context never survives into the other — same privacy class as the switch race. * Sample truth comes from the hub: listFlows' FlowSummary.sample (regenerated Dart SDK stubs) feeds the editor's sampleFlowNames; unknown (old hub / fetch failed) means no chips. Inside a sealed area the empty list offers the deliberate 'import example flows' action via chain flows import-samples against the instance's own dirs; the shared hub gets no such offer (it seeds samples itself, and a deliberate deletion is respected). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
64c2a77dc9
commit
d69277d29d
4 changed files with 107 additions and 1 deletions
|
|
@ -1002,6 +1002,7 @@ class HubService {
|
||||||
sizeBytes: f.sizeBytes.toInt(),
|
sizeBytes: f.sizeBytes.toInt(),
|
||||||
requiredCapabilities: List<String>.from(f.requiredCapabilities),
|
requiredCapabilities: List<String>.from(f.requiredCapabilities),
|
||||||
project: f.project,
|
project: f.project,
|
||||||
|
sample: f.sample,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList()
|
.toList()
|
||||||
|
|
@ -1593,12 +1594,19 @@ class SavedFlow {
|
||||||
/// project, then `general`).
|
/// project, then `general`).
|
||||||
final String project;
|
final String project;
|
||||||
|
|
||||||
|
/// True when the file still carries the bundled-sample header —
|
||||||
|
/// it arrived via the hub's sample import, not from an operator.
|
||||||
|
/// Old hubs without the wire field report false, so no chip is
|
||||||
|
/// ever shown on guesswork.
|
||||||
|
final bool sample;
|
||||||
|
|
||||||
const SavedFlow({
|
const SavedFlow({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.path,
|
required this.path,
|
||||||
required this.sizeBytes,
|
required this.sizeBytes,
|
||||||
required this.requiredCapabilities,
|
required this.requiredCapabilities,
|
||||||
this.project = '',
|
this.project = '',
|
||||||
|
this.sample = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,31 @@ class SystemActions {
|
||||||
return _runFai(['project', 'start', slug]);
|
return _runFai(['project', 'start', slug]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import the bundled sample flows (`chain flows import-samples`;
|
||||||
|
/// existing files are kept). With [sealedSlug] the import targets
|
||||||
|
/// that sealed instance's own data/modules dirs — sealed areas
|
||||||
|
/// start without samples, this is the deliberate pull.
|
||||||
|
static Future<({bool ok, String stdout, String stderr})>
|
||||||
|
chainFlowsImportSamples({String? sealedSlug}) async {
|
||||||
|
if (sealedSlug == null || sealedSlug.isEmpty) {
|
||||||
|
return _runFai(['flows', 'import-samples']);
|
||||||
|
}
|
||||||
|
final home =
|
||||||
|
Platform.environment['HOME'] ??
|
||||||
|
Platform.environment['USERPROFILE'] ??
|
||||||
|
'';
|
||||||
|
final sep = Platform.pathSeparator;
|
||||||
|
final root = '$home$sep.chain${sep}sealed$sep$sealedSlug';
|
||||||
|
return _runFai([
|
||||||
|
'flows',
|
||||||
|
'import-samples',
|
||||||
|
'--data-dir',
|
||||||
|
'$root${sep}data',
|
||||||
|
'--modules-dir',
|
||||||
|
'$root${sep}modules',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<({bool ok, String stdout, String stderr})> _runFai(
|
static Future<({bool ok, String stdout, String stderr})> _runFai(
|
||||||
List<String> args,
|
List<String> args,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
// need to know what's inside the editor any more.
|
// need to know what's inside the editor any more.
|
||||||
|
|
||||||
import 'dart:async' show unawaited;
|
import 'dart:async' show unawaited;
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
|
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
@ -20,6 +21,7 @@ import '../data/error_presentation.dart';
|
||||||
import '../data/flow_run_driver.dart';
|
import '../data/flow_run_driver.dart';
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
import '../data/store_caps.dart';
|
import '../data/store_caps.dart';
|
||||||
|
import '../data/system_actions.dart';
|
||||||
import '../data/workspace.dart';
|
import '../data/workspace.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../widgets/chain_install_confirm.dart';
|
import '../widgets/chain_install_confirm.dart';
|
||||||
|
|
@ -53,12 +55,19 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
/// editor would keep offering the OTHER hub's capabilities.
|
/// editor would keep offering the OTHER hub's capabilities.
|
||||||
String _connKey = Workspace.instance.activeSealed?.slug ?? '';
|
String _connKey = Workspace.instance.activeSealed?.slug ?? '';
|
||||||
|
|
||||||
|
/// Flow names the hub reports as bundled samples
|
||||||
|
/// (FlowSummary.sample). Null while unknown (fetch pending /
|
||||||
|
/// failed / hub too old) — the editor then shows no sample
|
||||||
|
/// chips rather than guessing.
|
||||||
|
Set<String>? _sampleNames;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_driver = StudioFlowRunDriver();
|
_driver = StudioFlowRunDriver();
|
||||||
_capabilities = _loadCapabilities();
|
_capabilities = _loadCapabilities();
|
||||||
_loadStoreCapabilities();
|
_loadStoreCapabilities();
|
||||||
|
_loadSampleNames();
|
||||||
Workspace.instance.addListener(_onWorkspaceChanged);
|
Workspace.instance.addListener(_onWorkspaceChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,9 +87,64 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_storeCaps = null; // other hub — snapshot unknown again
|
_storeCaps = null; // other hub — snapshot unknown again
|
||||||
|
_sampleNames = null;
|
||||||
_capabilities = _loadCapabilities();
|
_capabilities = _loadCapabilities();
|
||||||
});
|
});
|
||||||
_loadStoreCapabilities();
|
_loadStoreCapabilities();
|
||||||
|
_loadSampleNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull the hub's sample marking for the current connection.
|
||||||
|
/// Soft-fails to "unknown" — no chips on guesswork.
|
||||||
|
Future<void> _loadSampleNames() async {
|
||||||
|
final key = _connKey;
|
||||||
|
try {
|
||||||
|
final flows = await HubService.instance.listFlows();
|
||||||
|
if (!mounted || key != _connKey) return;
|
||||||
|
setState(() {
|
||||||
|
_sampleNames = {
|
||||||
|
for (final f in flows)
|
||||||
|
if (f.sample) f.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Unknown stays unknown.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flows directory of the CURRENT connection: a sealed area's
|
||||||
|
/// own instance dir, or null for the editor's shared-hub
|
||||||
|
/// default. Without this the editor kept listing the shared
|
||||||
|
/// hub's files while Studio was connected to a sealed area.
|
||||||
|
String? _flowsDirForConnection() {
|
||||||
|
final sealed = Workspace.instance.activeSealed;
|
||||||
|
if (sealed == null) return null;
|
||||||
|
final home =
|
||||||
|
Platform.environment['HOME'] ??
|
||||||
|
Platform.environment['USERPROFILE'] ??
|
||||||
|
'.';
|
||||||
|
return '$home/.chain/sealed/${sealed.slug}/data/flows';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deliberate sample pull for a sealed area's empty hub (the
|
||||||
|
/// shared hub seeds samples itself; a deliberate deletion there
|
||||||
|
/// must not be second-guessed with an import offer).
|
||||||
|
Future<void> _importSamples() async {
|
||||||
|
final sealed = Workspace.instance.activeSealed;
|
||||||
|
if (sealed == null) return;
|
||||||
|
final r = await SystemActions.chainFlowsImportSamples(
|
||||||
|
sealedSlug: sealed.slug,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!r.ok) {
|
||||||
|
showChainErrorSnack(
|
||||||
|
context,
|
||||||
|
'flows.importSamples',
|
||||||
|
(r.stderr.isEmpty ? r.stdout : r.stderr).trim(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _loadSampleNames();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadStoreCapabilities() async {
|
Future<void> _loadStoreCapabilities() async {
|
||||||
|
|
@ -210,7 +274,16 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
future: _capabilities,
|
future: _capabilities,
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
final caps = snap.data ?? const <String>[];
|
final caps = snap.data ?? const <String>[];
|
||||||
|
final sealed = Workspace.instance.activeSealed;
|
||||||
return FlowEditorPage(
|
return FlowEditorPage(
|
||||||
|
// A connection switch replaces the whole editor state:
|
||||||
|
// an open buffer from one context must never survive
|
||||||
|
// into the other (same privacy class as the switch
|
||||||
|
// race), and the file list re-roots at the new dir.
|
||||||
|
key: ValueKey('flow-editor-${sealed?.slug ?? ''}'),
|
||||||
|
flowsDir: _flowsDirForConnection(),
|
||||||
|
sampleFlowNames: _sampleNames,
|
||||||
|
onImportSamples: sealed == null ? null : _importSamples,
|
||||||
initialFlowName: widget.initialFlowName,
|
initialFlowName: widget.initialFlowName,
|
||||||
locale: editorLocale,
|
locale: editorLocale,
|
||||||
runDriver: _driver,
|
runDriver: _driver,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ packages:
|
||||||
path: "../fai_chain_studio_flow_editor"
|
path: "../fai_chain_studio_flow_editor"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.25.0"
|
version: "0.26.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue