inkpal_bridge 1.4.5 copy "inkpal_bridge: ^1.4.5" to clipboard
inkpal_bridge: ^1.4.5 copied to clipboard

Let AI agents inspect, debug, and control your running Flutter app — read widgets, tap, screenshot, capture errors. Zero deps. Debug-only.

example/lib/main.dart

/// inkpal_bridge example — multi-zone showcase.
///
/// Modelled on the InkPal "battlefield" battle-test app: each zone
/// exercises a specific bridge capability so an AI agent (or a human
/// reading this code) can immediately see what the bridge enables.
///
/// Zones:
///   • Counter zone     — basic state, taps, screenshot diff
///   • Forms zone       — text input + form validation
///   • List zone        — scroll, tap-by-text discovery
///   • Custom-widgets   — `walkerHooks` teaching the bridge to recognise a
///                         proprietary `BrandButton` widget
///
/// Run with:
///   `flutter run -d <device> --dart-define=INKPAL_LICENSE_KEY=ink_your_key_here`
library;

import 'package:flutter/material.dart';
import 'package:inkpal_bridge/inkpal_bridge.dart';

void main() {
  InkPalBridge.init(
    serverUrl: 'ws://localhost:8765',
    appRunner: () => runApp(const ExampleApp()),
    licenseKey: const String.fromEnvironment('INKPAL_LICENSE_KEY'),

    // Help the AI agent navigate by name
    knownRoutes: ['/', '/forms', '/list', '/custom'],
    routeDescriptions: {
      '/': 'Counter zone — basic stateful demo',
      '/forms': 'Forms zone — text input + validation',
      '/list': 'List zone — scrollable items',
      '/custom': 'Custom-widget zone — BrandButton demo',
    },

    // Expose state so /inkpal:state_capture has something to snapshot
    globalStateProvider: () async => {
      'time_ms': DateTime.now().millisecondsSinceEpoch,
      'theme': 'dark',
    },

    // Teach the semantics walker about proprietary widgets that don't
    // expose standard Material/Cupertino semantics. The AI agent can then
    // tap them by their human label without you adding `Semantics(label:)`
    // wrappers to every callsite.
    walkerHooks: InkPalWalkerHooks(
      isInteractiveWidget: (widget) => widget is BrandButton,
      extractTextFrom: (widget) =>
          widget is BrandButton ? widget.label : null,
    ),
  );
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'inkpal_bridge example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      // Standard bridge wiring — both lines are optional but enable
      // navigate_back + faster screenshots respectively.
      navigatorObservers: [
        if (InkPalBridge.instance != null)
          InkPalBridge.instance!.navigatorObserver,
      ],
      home: RepaintBoundary(
        key: InkPalBridge.instance?.repaintBoundaryKey,
        child: const HomeShell(),
      ),
      routes: {
        '/forms': (_) => const FormsZone(),
        '/list': (_) => const ListZone(),
        '/custom': (_) => const CustomWidgetsZone(),
      },
    );
  }
}

// ─── Home shell with zone navigation ────────────────────────────────────

class HomeShell extends StatefulWidget {
  const HomeShell({super.key});

  @override
  State<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('inkpal_bridge example')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Counter: $_counter',
              style: Theme.of(context).textTheme.headlineMedium,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FilledButton.tonal(
                  onPressed: () => setState(() => _counter--),
                  child: const Text('Decrement'),
                ),
                const SizedBox(width: 12),
                FilledButton(
                  onPressed: () => setState(() => _counter++),
                  child: const Text('Increment'),
                ),
              ],
            ),
            const Divider(height: 32),
            Text('Zones',
                style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            _ZoneTile('Forms', '/forms', Icons.edit_note),
            _ZoneTile('List', '/list', Icons.list_alt),
            _ZoneTile('Custom widgets (walkerHooks)', '/custom',
                Icons.widgets),
          ],
        ),
      ),
    );
  }
}

class _ZoneTile extends StatelessWidget {
  const _ZoneTile(this.label, this.route, this.icon);
  final String label;
  final String route;
  final IconData icon;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Icon(icon),
        title: Text(label),
        trailing: const Icon(Icons.chevron_right),
        onTap: () => Navigator.pushNamed(context, route),
      ),
    );
  }
}

// ─── Forms zone ─────────────────────────────────────────────────────────

class FormsZone extends StatefulWidget {
  const FormsZone({super.key});

  @override
  State<FormsZone> createState() => _FormsZoneState();
}

class _FormsZoneState extends State<FormsZone> {
  final _formKey = GlobalKey<FormState>();
  final _emailCtrl = TextEditingController();
  String? _result;

  @override
  void dispose() {
    _emailCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Forms')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextFormField(
                controller: _emailCtrl,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  hintText: 'you@example.com',
                ),
                validator: (v) =>
                    (v == null || !v.contains('@')) ? 'Invalid email' : null,
              ),
              const SizedBox(height: 16),
              FilledButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    setState(() => _result = 'Submitted: ${_emailCtrl.text}');
                  }
                },
                child: const Text('Submit'),
              ),
              if (_result != null) ...[
                const SizedBox(height: 24),
                Text(_result!, style: const TextStyle(color: Colors.green)),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

// ─── List zone ──────────────────────────────────────────────────────────

class ListZone extends StatelessWidget {
  const ListZone({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('List')),
      body: ListView.builder(
        itemCount: 50,
        itemBuilder: (_, i) => ListTile(
          leading: CircleAvatar(child: Text('${i + 1}')),
          title: Text('Item ${i + 1}'),
          subtitle: Text('Tap to see how the bridge resolves "Item ${i + 1}"'),
          onTap: () => ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Tapped Item ${i + 1}')),
          ),
        ),
      ),
    );
  }
}

// ─── Custom-widgets zone (the walkerHooks story) ────────────────────────

/// A proprietary widget that doesn't extend any standard Material widget.
/// Without `walkerHooks`, an AI agent walking the semantics tree would see
/// the inner GestureDetector but not know what this button is "called" or
/// that it's interactive. With `walkerHooks` (wired in main()), the agent
/// can ask "tap the Save button" and the bridge resolves it correctly.
class BrandButton extends StatelessWidget {
  const BrandButton({
    super.key,
    required this.label,
    required this.onPressed,
  });

  final String label;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onPressed,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        decoration: BoxDecoration(
          gradient: const LinearGradient(
            colors: [Color(0xFF6750A4), Color(0xFFB8A4E1)],
          ),
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          label,
          style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
        ),
      ),
    );
  }
}

class CustomWidgetsZone extends StatefulWidget {
  const CustomWidgetsZone({super.key});

  @override
  State<CustomWidgetsZone> createState() => _CustomWidgetsZoneState();
}

class _CustomWidgetsZoneState extends State<CustomWidgetsZone> {
  String _last = '(nothing yet)';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom widgets')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'These BrandButtons are proprietary widgets with no built-in '
              'Semantics. The walkerHooks wired in main() teach the AI '
              'agent to recognise them by label.',
            ),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                BrandButton(
                  label: 'Save',
                  onPressed: () => setState(() => _last = 'Save'),
                ),
                const SizedBox(width: 12),
                BrandButton(
                  label: 'Cancel',
                  onPressed: () => setState(() => _last = 'Cancel'),
                ),
              ],
            ),
            const SizedBox(height: 24),
            Center(
              child: BrandButton(
                label: 'Submit Order',
                onPressed: () => setState(() => _last = 'Submit Order'),
              ),
            ),
            const SizedBox(height: 32),
            Text('Last tapped: $_last', textAlign: TextAlign.center),
          ],
        ),
      ),
    );
  }
}
4
likes
160
points
512
downloads

Documentation

API reference

Publisher

verified publisherinkpal.ai

Weekly Downloads

Let AI agents inspect, debug, and control your running Flutter app — read widgets, tap, screenshot, capture errors. Zero deps. Debug-only.

Homepage
Repository (GitHub)
View/report issues

Topics

#mcp #ai #claude #copilot #agent

License

MIT (license)

Dependencies

flutter

More

Packages that depend on inkpal_bridge