ahp_flutter 0.1.0 copy "ahp_flutter: ^0.1.0" to clipboard
ahp_flutter: ^0.1.0 copied to clipboard

Flutter bindings for the Agent Host Protocol — scoped runtime, refcounted channel subscriptions, and rebuild narrowing for streaming state.

example/lib/main.dart

/// A small client for a running agent host.
///
/// Shows the three things the bindings exist for: scoping a runtime, watching
/// a channel, and narrowing rebuilds to the slice a widget renders.
///
///     code agent host --port 51234 --without-connection-token
///     flutter run -d macos
library;

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

/// Where the host is listening. Override with
/// `--dart-define=AHP_URL=ws://host:port`.
const _hostUrl = String.fromEnvironment(
  'AHP_URL',
  defaultValue: 'ws://localhost:51234',
);

final _root = Uri.parse('ahp-root://');

void main() => runApp(const AhpExampleApp());

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

  @override
  State<AhpExampleApp> createState() => _AhpExampleAppState();
}

class _AhpExampleAppState extends State<AhpExampleApp> {
  late final AhpRuntime _runtime;

  @override
  void initState() {
    super.initState();
    _runtime = AhpRuntime(
      // A factory, not a single client: it is called on every attempt, so a
      // reconnect an hour later can dial a fresh URL or a refreshed token.
      connect: () async => AhpConnection(
        await WebSocketAhpTransport.connect(Uri.parse(_hostUrl)),
      ),
      clientId: 'ahp-flutter-example',
    );
  }

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

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'AHP example',
    theme: ThemeData(
      colorSchemeSeed: Colors.indigo,
      brightness: Brightness.dark,
    ),
    home: AhpProvider(runtime: _runtime, child: const _HomePage()),
  );
}

class _HomePage extends StatelessWidget {
  const _HomePage();

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('Agent host'),
      bottom: const PreferredSize(
        preferredSize: Size.fromHeight(28),
        child: _ConnectionBar(),
      ),
    ),
    body: const Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _SessionCount(),
          SizedBox(height: 16),
          Expanded(child: _AgentList()),
        ],
      ),
    ),
  );
}

/// Watches the connection lifecycle.
///
/// The only widget here that depends on the connection scope, so the only one
/// that rebuilds when the connection transitions — which is the point of
/// splitting the runtime and its lifecycle into separate scopes.
class _ConnectionBar extends StatelessWidget {
  const _ConnectionBar();

  @override
  Widget build(BuildContext context) {
    final state = context.ahpConnection;
    final (label, color) = switch (state.phase) {
      AhpLifecycle.connected => ('connected', Colors.green),
      AhpLifecycle.connecting ||
      AhpLifecycle.handshaking => ('connecting', Colors.amber),
      AhpLifecycle.catchingUp => ('catching up', Colors.amber),
      AhpLifecycle.reconnecting => (
        'reconnecting (attempt ${state.attempt})',
        Colors.amber,
      ),
      AhpLifecycle.awaitingAuth => ('waiting for sign-in', Colors.orange),
      AhpLifecycle.fatal => ('cannot connect', Colors.red),
      AhpLifecycle.disconnected => ('disconnected', Colors.grey),
    };

    return Container(
      width: double.infinity,
      color: color.withValues(alpha: 0.15),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: Row(
        children: [
          Icon(Icons.circle, size: 8, color: color),
          const SizedBox(width: 8),
          Text(label),
          const Spacer(),
          if (state.protocolVersion case final version?)
            Text(
              'protocol $version',
              style: Theme.of(context).textTheme.bodySmall,
            ),
        ],
      ),
    );
  }
}

/// Narrows to one integer on the root channel.
///
/// Rebuilds only when the session count itself moves; an agent-list change, or
/// anything else arriving on the same channel, leaves this alone.
class _SessionCount extends StatelessWidget {
  const _SessionCount();

  @override
  Widget build(BuildContext context) => AhpSelector<RootState, int>(
    channel: _root,
    select: (state) => state.activeSessions ?? 0,
    builder: (context, count, _) => Text(
      '$count active ${count == 1 ? 'session' : 'sessions'}',
      style: Theme.of(context).textTheme.titleMedium,
    ),
  );
}

/// Narrows to the agent list, which changes far less often than the channel.
class _AgentList extends StatelessWidget {
  const _AgentList();

  @override
  Widget build(BuildContext context) => AhpSelector<RootState, List<AgentInfo>>(
    channel: _root,
    select: (state) => state.agents,
    builder: (context, agents, _) {
      if (agents.isEmpty) {
        return const Center(child: Text('No agents reported yet.'));
      }
      return ListView.separated(
        itemCount: agents.length,
        separatorBuilder: (_, _) => const Divider(height: 1),
        itemBuilder: (context, i) {
          final agent = agents[i];
          final models = agent.models.map((m) => m.id).join(', ');
          return ListTile(
            title: Text(agent.displayName),
            subtitle: Text(
              [agent.provider, if (models.isNotEmpty) models].join(' · '),
            ),
          );
        },
      );
    },
  );
}
1
likes
160
points
32
downloads

Documentation

Documentation
API reference

Publisher

verified publishermosesgameli.com

Weekly Downloads

Flutter bindings for the Agent Host Protocol — scoped runtime, refcounted channel subscriptions, and rebuild narrowing for streaming state.

Repository (GitHub)
View/report issues

Topics

#agent #protocol #state-management #websocket

License

MIT (license)

Dependencies

ahp_sdk, flutter

More

Packages that depend on ahp_flutter