// Single entry point for surfacing an error to the operator. // Every error pathway Studio shows (SnackBar, dialog, inline // strip) should go through one of these helpers so the // operator gets: // // • a copyable, selectable rendition of the message // • a friendly headline (gRPC codes mapped via // friendlyError) instead of a wall-of-stack-trace // • an automatic append to ~/.chain/logs/studio-errors.log so // the trail survives the dismiss action // // Call sites used to scatter `ScaffoldMessenger.of(ctx).showSnackBar( // SnackBar(content: Text(e.toString())))` — that violated the // "errors must be copyable" rule everywhere. These helpers // replace those open-coded sites. import 'package:flutter/material.dart'; import '../widgets/chain_error_box.dart'; import '../theme/tokens.dart'; import 'chain_log.dart'; /// Show [error] as a SnackBar with a copy affordance. /// /// [source] is a short tag (`flows.run`, `audit.clear`, …) /// used to group entries in the on-disk log so an operator or /// `chain doctor` can scan recent failures by area without /// reading the full free-text. Keep it stable; treat it like /// a metric label, not free-form text. /// /// [context] is the build-context the snack lands on. Falls /// back to a Material default duration of 6 seconds — twice /// the framework default so the operator has time to click /// copy on an unfamiliar message. void showFaiErrorSnack( BuildContext context, String source, Object error, { String? title, Duration duration = const Duration(seconds: 6), }) { // Fire-and-forget log write. If the disk's full or the dir // is read-only the UI still surfaces the error. ChainLog.instance.error(source, error, context: title); final messenger = ScaffoldMessenger.maybeOf(context); if (messenger == null) { // No Scaffold above — the caller is likely showing the // error from inside a dialog. We've already logged; bail // rather than throw. return; } messenger.hideCurrentSnackBar(); final theme = Theme.of(context); messenger.showSnackBar( SnackBar( duration: duration, behavior: SnackBarBehavior.floating, // Default SnackBar padding crushes our error box; reset // to zero and let the box's own padding take over. padding: EdgeInsets.zero, content: Padding( padding: const EdgeInsets.symmetric( horizontal: ChainSpace.sm, vertical: ChainSpace.xs, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (title != null) ...[ Text( title, style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.error, fontWeight: FontWeight.w600, ), ), const SizedBox(height: ChainSpace.xs), ], ChainErrorBox(error: error, isError: true), ], ), ), ), ); } /// Surface a failed external-process result (daemon start, binary /// probe, file-open) as a copyable error snack. The `ok/stdout/stderr` /// record these actions return is not a thrown error, so call sites used /// to drop it into a raw `SnackBar(Text(...))` — uncopyable, the exact /// thing the operator needs to paste back. This joins stderr + stdout /// into one selectable, one-tap-copyable, logged message. void showFaiProcessError( BuildContext context, String source, String stdout, String stderr, { String? title, }) { final detail = [stderr.trim(), stdout.trim()] .where((s) => s.isNotEmpty) .join('\n\n'); showFaiErrorSnack( context, source, detail.isEmpty ? 'process exited non-zero (no output)' : detail, title: title, ); } /// Like [showFaiProcessError] but as a persistent modal dialog. Use for /// failures the operator needs time to read and copy (a daemon that /// won't start) — a SnackBar flashes away before they can act on it. Future showFaiProcessErrorDialog( BuildContext context, String source, String stdout, String stderr, { String? title, }) { final detail = [stderr.trim(), stdout.trim()] .where((s) => s.isNotEmpty) .join('\n\n'); return showFaiErrorDialog( context, source, detail.isEmpty ? 'process exited non-zero (no output)' : detail, title: title, ); } /// Show [error] as a centered modal dialog. Use this when the /// failure blocks meaningful interaction (auth missing, hub /// unreachable) — a SnackBar would be too easy to dismiss. Future showFaiErrorDialog( BuildContext context, String source, Object error, { String? title, }) async { ChainLog.instance.error(source, error); return showDialog( context: context, builder: (ctx) => AlertDialog( title: title == null ? null : Text(title), content: SizedBox( width: 480, child: ChainErrorBox(error: error, isError: true, maxHeight: 240), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).maybePop(), child: const Text('OK'), ), ], ), ); }