nomos_flutter 0.55.6
nomos_flutter: ^0.55.6 copied to clipboard
Drive the real Nomos GitHolon from a Flutter app — a local-first domain runtime. iOS + macOS run the native-AOT kernel (no WebView memory cap); Android uses a hidden secure WebView. Write TypeScript d [...]
example/lib/main.dart
// nomos_flutter example — drives the REAL GitHolon through NomosScope. On iOS/macOS this runs the native-AOT
// kernel over dart:ffi (no WebView); on Android the hidden WebView host; on WEB the in-page @githolon/client
// browser runner (no FFI, no WebView). Same NomosBridge protocol on every platform.
import 'dart:ui' show PlatformDispatcher;
import 'package:flutter/material.dart';
import 'package:nomos_flutter/nomos_flutter.dart';
const cloud = 'https://nomos.captainapp.co.uk';
const workspace = 'todo-dogfood-20260616-codex';
const listId = 'prof-convergence';
void main() {
// Surface EVERY error legibly (the obfuscated web frames are unreadable): route framework + uncaught
// platform/async errors through a plain [web-proof] log so a throw anywhere in the write/watch path is
// visible in the browser console, not just as an opaque aBJ/aBK frame.
FlutterError.onError = (details) {
debugPrint('[web-proof] FLUTTER ERROR: ${details.exception}\n${details.stack}');
FlutterError.presentError(details);
};
WidgetsFlutterBinding.ensureInitialized();
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('[web-proof] UNCAUGHT: $error\n$stack');
return true; // handled — don't let it become an opaque uncaught frame
};
runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: _Root()));
}
class _Root extends StatelessWidget {
const _Root();
@override
Widget build(BuildContext context) => NomosScope(
cloud: cloud,
workspace: workspace,
builder: (context, nomos) => TodoPage(nomos.bridge),
loadingBuilder: (context, bridge) => const Scaffold(
body: Center(child: Text('booting the Nomos holon…')),
),
);
}
class TodoPage extends StatefulWidget {
final NomosBridge bridge;
const TodoPage(this.bridge, {super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final _input = TextEditingController();
String? _lastAdded; // the text of the most recent local add — pinned to the top so the proof is unambiguous
// Decode one watch row to its display text WITHOUT ever throwing (a bad row must not crash the ListView).
String _rowText(dynamic row) {
try {
if (row is Map) {
final data = row['data'];
final m = data is Map ? data : row;
return (m['text'] ?? m['title'] ?? '?').toString();
}
} catch (_) {}
return row.toString();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('nomos_flutter · todo')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Row(children: [
Expanded(child: TextField(controller: _input,
decoration: const InputDecoration(hintText: 'new todo…', border: OutlineInputBorder(), isDense: true))),
const SizedBox(width: 8),
FilledButton(onPressed: () async {
final t = _input.text.trim();
if (t.isEmpty) return;
_input.clear();
try {
// THE WRITE VERB — offerDirective (THE ONE ENCODER): hand the holon the PLANNED directive
// (domain/directiveId/payload) and it builds the v2 offer envelope via the single codec. Do
// NOT hand-encode + call offer(intentBytes:) — offer() expects opaque base64 envelope bytes,
// not raw JSON, so a hand-built JSON string is refused ("not a valid intent envelope").
debugPrint('[web-proof] offerDirective start text="$t"');
if (mounted) setState(() => _lastAdded = t);
final head = await widget.bridge.offerDirective(
domain: 'todo',
directiveId: 'addTodo',
payload: {'listId': listId, 'text': t},
);
debugPrint('[web-proof] offerDirective ok head=$head');
// Push the local write → session branch → edge admission → main (the write reaches the cloud).
debugPrint('[web-proof] sync start');
final r = await widget.bridge.sync(admit: true);
debugPrint('[web-proof] sync ok result=$r');
} catch (e, st) {
debugPrint('[web-proof] WRITE ERROR: $e\n$st');
// Surface the REAL error instead of an uncaught obfuscated exception.
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('add failed: $e')));
}
}
}, child: const Text('Add')),
]),
),
Expanded(
child: StreamBuilder<List<dynamic>>(
stream: widget.bridge.watch('todosByList', {'listId': listId}),
builder: (context, snap) {
if (snap.hasError) {
debugPrint('[web-proof] watch ERROR ${snap.error}');
return Center(child: Text('error: ${snap.error}'));
}
final rows = List<dynamic>.from(snap.data ?? const []);
// Decode to display texts (never throws), then pin the most recent local add to the TOP so a
// new todo is unambiguously visible even among 100+ existing rows.
final texts = rows.map(_rowText).toList();
final pinned = _lastAdded;
final foundPinned = pinned != null && texts.contains(pinned);
debugPrint(
'[web-proof] watch rows=${texts.length} lastAdded=$pinned present=$foundPinned');
if (foundPinned) {
texts.remove(pinned);
texts.insert(0, pinned);
}
return ListView.builder(
itemCount: texts.length,
itemBuilder: (c, i) {
final isPinned = foundPinned && i == 0;
return ListTile(
dense: true,
leading: isPinned ? const Icon(Icons.check_circle, color: Colors.green) : null,
title: Text(texts[i],
style: isPinned ? const TextStyle(fontWeight: FontWeight.bold) : null),
);
},
);
},
),
),
],
),
);
}
}