diff --git a/lib/src/flow_editor_page.dart b/lib/src/flow_editor_page.dart index 6e1167f..400235c 100644 --- a/lib/src/flow_editor_page.dart +++ b/lib/src/flow_editor_page.dart @@ -49,7 +49,7 @@ String _defaultFlowsDir() { Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.'; - return '$home/.chain/data/flows'; + return '$home/.fai/data/flows'; } class FlowEditorPage extends StatefulWidget { @@ -123,11 +123,6 @@ class _FlowEditorPageState extends State late Future> _files; late final TabController _tabs; - /// Per-file scan cache (example marker + required caps), - /// keyed by path + mtime. Populated during [_listFiles] so the - /// file rows never re-read from disk on a rebuild/paint. - final _FlowMetaCache _flowMetaCache = _FlowMetaCache(); - /// Live hover overlay — inserted when the pointer enters an /// underlined issue range, removed when it leaves both the /// range and the tooltip card. Kept on State so we can clear @@ -269,23 +264,20 @@ class _FlowEditorPageState extends State .where((e) => e is File && e.path.endsWith('.yaml')) .cast() .toList(); - final files = <_FlowFile>[]; - for (final f in entries) { - final stat = f.statSync(); - // Per-file scan (example marker + required caps) is cached - // by path + mtime so a refresh that didn't touch a file - // doesn't re-read it, and a paint never re-reads at all. - final meta = await _flowMetaCache.forFile(f, stat); - files.add( - _FlowFile( - name: f.uri.pathSegments.last.replaceAll(RegExp(r'\.yaml$'), ''), - path: f.path, - sizeBytes: stat.size, - meta: meta, - ), - ); - } - files.sort((a, b) => a.name.compareTo(b.name)); + final files = + entries + .map( + (f) => _FlowFile( + name: f.uri.pathSegments.last.replaceAll( + RegExp(r'\.yaml$'), + '', + ), + path: f.path, + sizeBytes: f.statSync().size, + ), + ) + .toList() + ..sort((a, b) => a.name.compareTo(b.name)); return files; } @@ -423,40 +415,6 @@ outputs: setState(() => _files = _listFiles()); } - /// Install the capabilities a flow row flagged as missing, - /// reusing the host's existing [FlowEditorPage.onInstallCapability] - /// path — no new install mechanism. Installs sequentially, - /// keeps the latest capability list, refreshes the analyzer - /// providers, and re-scans the file list so the row's badge - /// updates. Returns once all installs settle. - Future _installMissingCaps(List caps) async { - final handler = widget.onInstallCapability; - if (handler == null || caps.isEmpty) return; - List? latest; - for (final cap in caps) { - try { - final result = await handler(cap); - if (result != null) latest = result; - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(e.toString()))); - } - } - if (!mounted) return; - if (latest != null) { - _controller.codeController.setCapabilityProviders( - available: () => latest!, - store: () => widget.storeCapabilities, - strings: AnalyzerStrings.from(_l), - ); - await _controller.codeController.reanalyze(); - if (!mounted) return; - } - await _refreshFiles(); - } - Future _addStep() async { final picked = await CapabilityPicker.show( context, @@ -546,14 +504,8 @@ outputs: filesFuture: _files, activeName: _controller.activeName, strings: _l, - installedNames: _installedNames( - widget.availableCapabilities, - ), onOpen: _openFile, onRefresh: _refreshFiles, - onInstallMissing: widget.onInstallCapability == null - ? null - : _installMissingCaps, ), ), const VerticalDivider(width: 1), @@ -1092,107 +1044,13 @@ class _FlowFile { final String name; final String path; final int sizeBytes; - - /// Scan result for this file — whether it's a bundled example - /// and which capabilities its steps require. Computed once at - /// list-load time (see [_FlowMetaCache]). - final _FlowMeta meta; - const _FlowFile({ required this.name, required this.path, required this.sizeBytes, - required this.meta, }); } -/// Marker text every bundled sample flow carries in its -/// provenance comment header. A file is an example iff its raw -/// content contains this exact string. -const String _sampleFlowMarker = 'F∆I sample flow'; - -/// Built-in capabilities the Hub always provides, even when -/// they aren't in the host-supplied installed list. Treated as -/// satisfied so the "needs N modules" badge never flags them. -const Set _builtinCapabilities = {'debug.echo'}; - -/// Result of scanning a single flow file: provenance + the -/// capability NAMES (without `@version`) its steps reference. -class _FlowMeta { - final bool isExample; - final List requiredCaps; - const _FlowMeta({required this.isExample, required this.requiredCaps}); - - static const empty = _FlowMeta(isExample: false, requiredCaps: []); - - /// Capabilities this flow needs that are neither installed nor - /// built in. [availableNames] is the set of installed - /// capability NAMES (the part before `@`). - List missingCaps(Set availableNames) => requiredCaps - .where( - (c) => - !availableNames.contains(c) && !_builtinCapabilities.contains(c), - ) - .toList(); -} - -/// Caches [_FlowMeta] per file, keyed by path + mtime. Reading -/// and scanning a flow's YAML happens here, only when the file -/// is new or changed since the last list load — never on a -/// paint. -class _FlowMetaCache { - final Map _byPath = {}; - - Future<_FlowMeta> forFile(File f, FileStat stat) async { - final cached = _byPath[f.path]; - if (cached != null && cached.mtime == stat.modified) { - return cached.meta; - } - _FlowMeta meta; - try { - final text = await f.readAsString(); - meta = _scanFlow(text); - } catch (_) { - meta = _FlowMeta.empty; - } - _byPath[f.path] = (mtime: stat.modified, meta: meta); - return meta; - } -} - -/// Scan raw flow YAML for its example marker and the capability -/// ids referenced by `use:` lines. A line scan is used rather -/// than a full YAML parse: it's robust against malformed flows -/// (the analyzer reports those separately) and never throws. -_FlowMeta _scanFlow(String text) { - final isExample = text.contains(_sampleFlowMarker); - final caps = {}; - final useRe = RegExp(r'^\s*-?\s*use:\s*(.+?)\s*$'); - for (final raw in text.split('\n')) { - // Skip comment-only lines. - final line = raw.split('#').first; - final m = useRe.firstMatch(line); - if (m == null) continue; - var value = m.group(1)!.trim(); - // Strip surrounding quotes if present. - if (value.length >= 2 && - ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")))) { - value = value.substring(1, value.length - 1); - } - // Drop the @ suffix, keep the capability name. - final name = value.split('@').first.trim(); - if (name.isNotEmpty) caps.add(name); - } - return _FlowMeta(isExample: isExample, requiredCaps: caps.toList()); -} - -/// Reduce the host-supplied installed list (entries like -/// `text.extract@0.1.0`) to the set of bare capability NAMES so -/// satisfaction is checked name-first, version-agnostic. -Set _installedNames(List available) => - available.map((c) => c.split('@').first).toSet(); - /// Compact byte-count formatter used in the flow-list rows. /// '12 B' / '4.3 KB' / '1.2 MB'. Avoids overflowing the /// narrow sidebar with raw byte counts on long flows. @@ -1208,26 +1066,15 @@ class _FileList extends StatelessWidget { final Future> filesFuture; final String? activeName; final FlowEditorStrings strings; - - /// Bare capability NAMES the host reports as installed. Used - /// to compute each row's missing-module count. - final Set installedNames; final void Function(_FlowFile) onOpen; final VoidCallback onRefresh; - /// Install the listed missing capabilities. `null` when the - /// host wired no install handler — the warning badge still - /// renders, just without the one-click action. - final Future Function(List)? onInstallMissing; - const _FileList({ required this.filesFuture, required this.activeName, required this.strings, - required this.installedNames, required this.onOpen, required this.onRefresh, - required this.onInstallMissing, }); @override @@ -1307,7 +1154,6 @@ class _FileList extends StatelessWidget { itemBuilder: (_, i) { final f = files[i]; final isActive = f.name == activeName; - final missing = f.meta.missingCaps(installedNames); return InkWell( onTap: () => onOpen(f), child: Container( @@ -1376,25 +1222,6 @@ class _FileList extends StatelessWidget { fontSize: 11, ), ), - if (f.meta.isExample || missing.isNotEmpty) ...[ - const SizedBox(height: FaiSpace.xs), - Wrap( - spacing: FaiSpace.xs, - runSpacing: FaiSpace.xs, - children: [ - if (f.meta.isExample) - _ExampleBadge(strings: strings), - if (missing.isNotEmpty) - _MissingModulesBadge( - missing: missing, - strings: strings, - onInstall: onInstallMissing == null - ? null - : () => onInstallMissing!(missing), - ), - ], - ), - ], ], ), ), @@ -1409,108 +1236,6 @@ class _FileList extends StatelessWidget { } } -/// Quiet "Example" / "Beispiel" chip on bundled sample flows. -/// Tinted from the surface palette so it reads as metadata, not -/// an alert — stays subtle in both light and dark themes. -class _ExampleBadge extends StatelessWidget { - final FlowEditorStrings strings; - const _ExampleBadge({required this.strings}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final fg = theme.colorScheme.onSurfaceVariant; - return Tooltip( - message: strings.flowListExampleTooltip, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(FaiRadius.sm), - border: Border.all( - color: theme.colorScheme.outlineVariant.withValues(alpha: 0.6), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.auto_awesome_outlined, size: 10, color: fg), - const SizedBox(width: 4), - Text( - strings.flowListExampleBadge, - style: theme.textTheme.labelSmall?.copyWith( - color: fg, - fontSize: 10, - letterSpacing: 0.2, - ), - ), - ], - ), - ), - ); - } -} - -/// Warning-toned "needs N modules" chip on flows whose steps -/// reference capabilities that aren't installed. Tooltip lists -/// the exact missing ids. When an install handler is wired, the -/// chip is tappable and one-click installs all missing caps via -/// the host's existing install path. -class _MissingModulesBadge extends StatelessWidget { - final List missing; - final FlowEditorStrings strings; - final VoidCallback? onInstall; - const _MissingModulesBadge({ - required this.missing, - required this.strings, - required this.onInstall, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // Amber warning tone consistent with the diagnostic strip's - // warning colour, readable on both light + dark surfaces. - const warn = Color(0xFFEF6C00); - final label = '${strings.flowListNeedsModules(missing.length)}' - '${onInstall != null ? ' · ${strings.flowListInstallMissing}' : ''}'; - final chip = Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), - decoration: BoxDecoration( - color: warn.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(FaiRadius.sm), - border: Border.all(color: warn.withValues(alpha: 0.5)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.extension_off_outlined, size: 10, color: warn), - const SizedBox(width: 4), - Text( - label, - style: theme.textTheme.labelSmall?.copyWith( - color: warn, - fontSize: 10, - fontWeight: FontWeight.w600, - letterSpacing: 0.2, - ), - ), - ], - ), - ); - return Tooltip( - message: strings.flowListNeedsModulesTooltip(missing.join('\n')), - child: onInstall == null - ? chip - : InkWell( - onTap: onInstall, - borderRadius: BorderRadius.circular(FaiRadius.sm), - child: chip, - ), - ); - } -} - // --- empty state --- class _EmptyState extends StatelessWidget { diff --git a/lib/src/l10n.dart b/lib/src/l10n.dart index a549432..0424b0f 100644 --- a/lib/src/l10n.dart +++ b/lib/src/l10n.dart @@ -245,25 +245,6 @@ class FlowEditorStrings { ); String analyzerYamlError(String detail) => _t('YAML: $detail', 'YAML: $detail'); - // Flow-list row badges — example provenance + missing-module - // warning. Kept short so they fit the narrow sidebar. - String get flowListExampleBadge => _t('Example', 'Beispiel'); - String get flowListExampleTooltip => _t( - 'Bundled sample flow.', - 'Mitgelieferter Beispiel-Flow.', - ); - String flowListNeedsModules(int n) => _t( - n == 1 ? 'needs 1 module' : 'needs $n modules', - n == 1 ? 'braucht 1 Modul' : 'braucht $n Module', - ); - String flowListNeedsModulesTooltip(String caps) => _t( - 'This flow needs capabilities that are not installed:\n$caps\n' - 'Click to install the missing ones.', - 'Dieser Flow braucht nicht installierte Capabilities:\n$caps\n' - 'Klicken, um die fehlenden zu installieren.', - ); - String get flowListInstallMissing => _t('Install', 'Installieren'); - // Quick-fix button labels. String fixInstallCap(String cap) => _t('Install $cap', '$cap installieren'); String fixAddSource(String cap) => diff --git a/lib/src/model/layout_store.dart b/lib/src/model/layout_store.dart index a80964d..a9067e5 100644 --- a/lib/src/model/layout_store.dart +++ b/lib/src/model/layout_store.dart @@ -18,7 +18,7 @@ // // Storage layout: // -// ~/.chain/data/flows/.layout/.json +// ~/.fai/data/flows/.layout/.json // // One file per flow. JSON shape: // @@ -93,7 +93,7 @@ class NodePosition { /// graph view loads / saves them at well-defined moments /// (open file, drag end), not in the render loop. class LayoutStore { - /// Directory `~/.chain/data/flows/.layout/`, created on first + /// Directory `~/.fai/data/flows/.layout/`, created on first /// write. Hidden so it doesn't show up in `fai admin /// flows list` style enumerations. static String defaultDir() { @@ -101,7 +101,7 @@ class LayoutStore { Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.'; - return '$home/.chain/data/flows/.layout'; + return '$home/.fai/data/flows/.layout'; } /// Load the layout for [flowName]. Returns an empty layout diff --git a/lib/src/widgets/run_tab.dart b/lib/src/widgets/run_tab.dart index 5fa692d..69f65ba 100644 --- a/lib/src/widgets/run_tab.dart +++ b/lib/src/widgets/run_tab.dart @@ -15,7 +15,7 @@ // 4. The flow's outputs once the run resolves. // // The tab requires the file to be saved on disk — the hub's -// runSavedFlow reads from `~/.chain/data/flows/.yaml`, +// runSavedFlow reads from `~/.fai/data/flows/.yaml`, // not from the in-memory buffer. The dirty banner reminds // the operator to save before running so they don't run a // stale version by surprise.