diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 93f253f..9ff0af9 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -1002,6 +1002,7 @@ class HubService { sizeBytes: f.sizeBytes.toInt(), requiredCapabilities: List.from(f.requiredCapabilities), project: f.project, + sample: f.sample, ), ) .toList() @@ -1593,12 +1594,19 @@ class SavedFlow { /// project, then `general`). 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({ required this.name, required this.path, required this.sizeBytes, required this.requiredCapabilities, this.project = '', + this.sample = false, }); } diff --git a/lib/data/system_actions.dart b/lib/data/system_actions.dart index a562091..0a3a96f 100644 --- a/lib/data/system_actions.dart +++ b/lib/data/system_actions.dart @@ -239,6 +239,31 @@ class SystemActions { 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( List args, ) async { diff --git a/lib/pages/flows.dart b/lib/pages/flows.dart index b68c05e..df948e2 100644 --- a/lib/pages/flows.dart +++ b/lib/pages/flows.dart @@ -11,6 +11,7 @@ // need to know what's inside the editor any more. import 'dart:async' show unawaited; +import 'dart:io' show Platform; import 'package:chain_studio_flow_editor/chain_studio_flow_editor.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/hub.dart'; import '../data/store_caps.dart'; +import '../data/system_actions.dart'; import '../data/workspace.dart'; import '../l10n/app_localizations.dart'; import '../widgets/chain_install_confirm.dart'; @@ -53,12 +55,19 @@ class _FlowsPageState extends State { /// editor would keep offering the OTHER hub's capabilities. 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? _sampleNames; + @override void initState() { super.initState(); _driver = StudioFlowRunDriver(); _capabilities = _loadCapabilities(); _loadStoreCapabilities(); + _loadSampleNames(); Workspace.instance.addListener(_onWorkspaceChanged); } @@ -78,9 +87,64 @@ class _FlowsPageState extends State { if (!mounted) return; setState(() { _storeCaps = null; // other hub — snapshot unknown again + _sampleNames = null; _capabilities = _loadCapabilities(); }); _loadStoreCapabilities(); + _loadSampleNames(); + } + + /// Pull the hub's sample marking for the current connection. + /// Soft-fails to "unknown" — no chips on guesswork. + Future _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 _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 _loadStoreCapabilities() async { @@ -210,7 +274,16 @@ class _FlowsPageState extends State { future: _capabilities, builder: (context, snap) { final caps = snap.data ?? const []; + final sealed = Workspace.instance.activeSealed; 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, locale: editorLocale, runDriver: _driver, diff --git a/pubspec.lock b/pubspec.lock index 8235b00..81a6014 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -46,7 +46,7 @@ packages: path: "../fai_chain_studio_flow_editor" relative: true source: path - version: "0.25.0" + version: "0.26.0" characters: dependency: transitive description: