LogoSkills

marionette-guide Templates

Drop-in code templates for wiring Marionette into a Flutter app that uses CoUI.

Marionette CoUI Binding โ€” Templates#

Drop-in code templates for wiring Marionette into a Flutter app that uses CoUI.

1. main.dart โ€” full binding setup#

import 'package:coui_flutter/coui_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:marionette_logger/marionette_logger.dart';
import 'package:logger/logger.dart';

const bool _isFlutterTest =
    bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);

void main() {
  if (kDebugMode && !_isFlutterTest) {
    final logCollector = LoggerLogCollector();
    MarionetteBinding.ensureInitialized(
      MarionetteConfiguration(
        logCollector: logCollector,
        isInteractiveWidget: _isCoUIInteractive,
        extractText: _extractCoUIText,
      ),
    );

    Logger.level = Level.debug;
    final _ = Logger(
      output: MultiOutput([ConsoleOutput(), logCollector]),
    );
  } else {
    WidgetsFlutterBinding.ensureInitialized();
  }

  runApp(const App());
}

bool _isCoUIInteractive(Type type) =>
    // Buttons
    type == PrimaryButton ||
    type == SecondaryButton ||
    type == OutlineButton ||
    type == GhostButton ||
    type == DestructiveButton ||
    type == LinkButton ||
    type == TextButton ||
    type == IconButton ||
    // Inputs
    type == Input ||
    type == Textarea ||
    type == DatePicker ||
    type == Select ||
    // Toggles
    type == Toggle ||
    type == Switch ||
    type == Checkbox ||
    type == Radio ||
    type == Slider ||
    // Navigation primitives that are tappable
    type == TabItem ||
    type == AccordionItem ||
    type == MenuItem;

String? _extractCoUIText(Element element) {
  final widget = element.widget;

  // Buttons โ€” labels live in child Text widgets
  if (widget is PrimaryButton ||
      widget is SecondaryButton ||
      widget is OutlineButton ||
      widget is GhostButton ||
      widget is DestructiveButton ||
      widget is LinkButton ||
      widget is TextButton) {
    return _readDescendantText(element);
  }

  // Inputs โ€” prefer the current value, fall back to placeholder
  if (widget is Input) {
    final ctrl = widget.controller;
    if (ctrl != null && ctrl.text.isNotEmpty) return ctrl.text;
    return widget.placeholder;
  }
  if (widget is Textarea) {
    final ctrl = widget.controller;
    if (ctrl != null && ctrl.text.isNotEmpty) return ctrl.text;
    return widget.placeholder;
  }

  return null;
}

String? _readDescendantText(Element root) {
  final buffer = StringBuffer();
  void visit(Element e) {
    final w = e.widget;
    if (w is Text && w.data != null) {
      if (buffer.isNotEmpty) buffer.write(' ');
      buffer.write(w.data);
      return;
    }
    if (w is RichText) {
      final plain = w.text.toPlainText();
      if (plain.isNotEmpty) {
        if (buffer.isNotEmpty) buffer.write(' ');
        buffer.write(plain);
      }
      return;
    }
    e.visitChildren(visit);
  }

  root.visitChildren(visit);
  final out = buffer.toString().trim();
  return out.isEmpty ? null : out;
}

2. pubspec.yaml additions#

dev_dependencies:
  marionette_flutter: ^0.6.0      # binding for the app
  marionette_logger: ^0.6.0       # if you use the `logger` package
  marionette_mcp: ^0.6.0          # MCP server โ€” global activation also fine

marionette_flutter, marionette_logger, and marionette_mcp (plus marionette_cli/marionette_logging) are released in lockstep from one monorepo โ€” always pin them to the same version. connect checks the binding version against the server version and errors on a mismatch.

3. Claude Code .mcp.json (per-project)#

{
   " mcpServers " : {
     " marionette " : {
       " command " :  " marionette_mcp " ,
       " args " : []
    }
  }
}

4. Smoke-test prompt#

Connect to the running app via Marionette (URI:  < paste from `flutter run` > ).
For each tab in the bottom NavigationBar, tap it, wait for the route to
settle, call get_interactive_elements, and take_screenshots. Save the
results to .claude/docs/marionette/smoke- < timestamp > /. If any tab throws,
collect get_logs and stop with the captured error.

5. Widgetbook regression prompt#

The app is currently running CoUI Widgetbook on the connected VM Service.
Use NavigationSidebar in the Widgetbook UI to walk every UseCase in order.
For each UseCase:
  1. take_screenshots โ†’ screens/ < usecase > _idle.png
  2. get_interactive_elements
  3. For each interactive element, perform its default action
     (button โ†’ tap, input โ†’ enter  " marionette-test " , toggle โ†’ flip).
  4. take_screenshots โ†’ screens/ < usecase > _after.png
  5. If the two screenshots are identical, flag the UseCase as
      " non-responsive "   in summary.md.
Stop after 100 UseCases or on first unhandled error.

6. Custom extension with schema (0.6.0+)#

For app-specific actions Marionette can't derive from the widget tree โ€” reading BLoC state, driving a PageController/TabController directly, seeding a fixture โ€” register an extension once in main.dart (inside the same kDebugMode guard as MarionetteBinding.ensureInitialized):

import 'package:marionette_flutter/marionette_flutter.dart';

void _registerAppExtensions() {
  registerMarionetteExtension(
    'goToTab',
    (args) async {
      final index = int.tryParse(args['index'] ?? '');
      if (index == null) {
        return MarionetteExtensionResult.invalidParams('index must be an int');
      }
      appTabController.animateTo(index);
      return MarionetteExtensionResult.success({'ok': 'true'});
    },
    inputSchema: ExtensionInputSchema({
      'index': ExtensionParam.integer(description: 'Tab index to jump to'),
    }),
  );
}
  • Params are flat scalars only (ExtensionParam.string/.integer/.number/.boolean) โ€” no nested objects/arrays.
  • With inputSchema, the extension appears as its own validated tool (name sanitized to [a-z0-9_-], e.g. goToTab โ†’ go_to_tab); without it, it's only reachable via list_custom_extensions/call_custom_extension.
  • Don't prefix the name with ext.flutter. โ€” added automatically, and doing it yourself throws ArgumentError.