inkpal_bridge 7.0.0
inkpal_bridge: ^7.0.0 copied to clipboard
Turn your running Flutter app into an MCP server. Claude Code, Cursor, Windsurf, Codex, Copilot can inspect, drive, hot-reload it. 69 tools over local HTTP. Zero deps.
7.0.0 — final stable #
Consolidates the 6.1 → 6.9 phased ladder into a blessed stable release. Everything that shipped between 6.0 and 6.9 is preserved verbatim.
What 7.0.0 adds on top of 6.9 #
inkpal_release_readiness— one-shot pre-flight health check aggregating build mode, navigator binding, UI reachability, error catcher, state adapter, and app router. Returnsready_to_driveplus per-check details and a blockers list. Cheap, side-effect free, designed to be the first call an agent makes.
Consolidated feature map (6.1 → 7.0) #
- 6.1 — hot-reload driver rewrite (single-listener-per-socket +
shared response map keyed by JSON-RPC id), zone-safe bootstrap,
scroll
reveal:alias, tapexpect_frame_stampguard. - 6.2 — verification envelopes on every mutating action
(
screen_changed,added_labels,removed_labels,frame_stamp,route_before/route_after,errors_since_action). - 6.3 — HTTP memory:
get_http_log,assert_no_failed_requests,wait_for_request. - 6.4 — navigation memory:
open_deeplink,get_route_graph,coverage_report. - 6.5 — live state adapter.
InkPalStateAdapterwith optionallist/read/write/invalidatecallbacks; four new state_* tools. Zero-dep, duck-typed. - 6.6 — batched state (Bloc / Provider ergonomics):
state_read_many,state_all, filteredstate_providers. - 6.7 — reactive state (GetX / MobX / Bloc streams):
state_watch,state_capabilities. - 6.8 — device-side pixel diff:
screenshot_save,screenshot_diff,screenshot_list. Pure-dart:uipixel_diff.dartengine, per-channel threshold, bbox output. - 6.9 — reference-PNG compare (
screenshot_compare_ref) and bundleddart run inkpal_bridge:connectdiscovery CLI. - 7.0 —
release_readinessaggregate check + consolidated release blessing.
Compatibility contract #
- 6.0 baseline preserved verbatim. Every 6.0 integration test continues to pass unchanged.
- No parameters removed from
InkPalBridge.init/inkpalRunApp. Every new param is nullable with a null default. - No handler response shape reduced. New fields added additively;
the
verification:envelope on mutating actions is additive to the existing result map (spread, not replaced). - Deprecated
licenseKey:/apiUrl:params remain accepted-and- ignored no-ops (planned removal in 8.0). - Release-mode short-circuit still runs
appRunner()synchronously with zero bridge overhead. Verified by theif (kReleaseMode)branches at the top ofinkpalRunAppandInkPalBridge.init.
Migration from 6.x #
Bump your dependency constraint. That's it.
dependencies:
inkpal_bridge: ^7.0.0
No code changes. No new required params. No renames.
Verification #
- 37/37
mcp_server_test.darttests pass. - 4/4
pixel_diff_test.dartengine tests pass. dart analyze lib binclean (zero issues).- 60 → 69 MCP tools registered.
- Alias-drift guard exercises every one of the 69 tools against a registered handler in one CI run.
Tool count: 68 → 69. #
6.9.0 #
Ninth step on the ladder — last stop before 7.0.0. Adds
reference-PNG compare (no in-memory store required) and a bundled
connect CLI that discovers the running bridge and prints editor
config.
New #
inkpal_screenshot_compare_ref— pixel-diff the current screen against a caller-supplied reference PNG (base64-encoded). Uses the same engine asscreenshot_difffrom 6.8. Rejects non-PNG blobs by magic-byte check.dart run inkpal_bridge:connect— zero-config discovery helper. Scans ports 8767..8770 (overridable) for a live MCP server, then prints copy-paste editor config for Claude Desktop, Cursor, Windsurf, Copilot.--jsonfor machine-readable output.
Field loop #
inkpal_screenshot_compare_ref({
reference_png_b64: <figma_export.png>,
threshold: 20,
})
No file I/O, no reference store to manage, no side effects.
Compatibility #
- 32/32
mcp_server_test.darttests pass + 4/4 pixel-diff engine tests. 6.0 baseline preserved verbatim.bin/connect.dartis a standalone Dart executable — has no impact on the runtime bridge.
Tool count: 67 → 68. #
6.8.0 #
Eighth step on the ladder. Device-side pixel diff — closes the "did
my tap actually change something visible?" loop without shelling out
to pixelmatch or ADB screencap on a host.
New #
inkpal_screenshot_save— capture a PNG and stash it in the bridge's in-memory store undername(default "default"). Cleared on dispose(), no file I/O.inkpal_screenshot_diff— capture now, compare against a saved screenshot. Fast path returnsidentical: trueon byte-equal PNGs; otherwise decodes both viadart:uiand returnschanged_pixels,changed_percent,changed_bbox,size_delta_bytes. Per-channel RGBA threshold (default 30) — alpha ignored.inkpal_screenshot_list— enumerate the saved screenshots (names, count, total bytes).
Engine #
- New
lib/src/inspection/pixel_diff.dart— puredart:uidecoder + per-pixel loop. When dimensions differ, the intersection is compared and the extra area outside is counted as fully changed (matches pixelmatch semantics). - 4 engine tests exercise identical / all-different / below-threshold / dimension-mismatch shapes.
Loop #
inkpal_screenshot_save({name: 'baseline'})
inkpal_tap({text: 'Toggle dark mode'})
inkpal_wait_for_idle()
inkpal_screenshot_diff({against: 'baseline', threshold: 20})
→ {changed_percent: 87.4, changed_bbox: {left: 0, top: 0, right: 359, bottom: 799}}
Compatibility #
- 31/31
mcp_server_test.darttests pass. 6.0 baseline preserved verbatim. Existingtake_screenshothandler unchanged.
Tool count: 64 → 67. #
6.7.0 #
Seventh step on the ladder. Reactive state — GetX Rx / MobX
@observable / Bloc streams. Same InkPalStateAdapter from 6.5,
two new primitives that turn "read-once" into "wait for a change."
New #
inkpal_state_watch— wait until a provider's value changes, up totimeout_ms. Adapter-agnostic — the bridge pollsread()atpoll_ms(default 100ms) and JSON-compares. Optionalexpectedvalue: return immediately if the current read already differs from what the caller thought.inkpal_state_capabilities— describe what the adapter supports (list/read/write/invalidate/watch). Editors call this once to avoid asking for surfaces the app didn't wire.
GetX example #
final cart = CartController(); // extends GetxController, cart.items is RxList
inkpalRunApp(
const MyApp(),
stateAdapter: InkPalStateAdapter(
list: () async => [
{'id': 'cart.count', 'kind': 'Rx<int>'},
{'id': 'auth.user', 'kind': 'Rx<User?>'},
],
read: (id) async => switch (id) {
'cart.count' => {'value': cart.items.length},
'auth.user' => {'value': auth.user.value?.email},
_ => {'error': 'unknown provider: \$id'},
},
// No write / invalidate — GetX prefers explicit controller methods.
),
);
Then in the agent:
inkpal_tap({text: 'Add to cart'})
inkpal_state_watch({id: 'cart.count', timeout_ms: 2000})
MobX example #
inkpalRunApp(
const MyApp(),
stateAdapter: InkPalStateAdapter(
list: () async => [
{'id': 'ui.dark', 'kind': '@observable bool'},
],
read: (id) async => id == 'ui.dark'
? {'value': uiStore.darkMode}
: {'error': 'unknown provider: \$id'},
write: (id, value) async {
if (id == 'ui.dark') {
uiStore.setDarkMode(value as bool);
return {'success': true};
}
return {'success': false};
},
),
);
Compatibility #
- 29/29
mcp_server_test.dartpass. 6.0 baseline preserved verbatim. Adapter-agnostic — no library-specific plumbing added.
Tool count: 62 → 64. #
6.6.0 #
Sixth step on the ladder. Bloc / Cubit / Provider ergonomics — the
same InkPalStateAdapter from 6.5 gains batched primitives so agents
can snapshot the whole state tree in one round-trip, and filter the
provider list on the bridge side without teaching the adapter about
filters.
New #
inkpal_state_read_many— batch-read N providers by id, or read every providerlist()returns whenidsis omitted. Errors surface per-id so one bad read never poisons the whole batch.inkpal_state_all— convenience: fuseslist()+read()into one grid. Requires both callbacks on the adapter.inkpal_state_providers— now accepts optionalfilter(substring match on id / label / kind), bridge-side. The adapter still returns everything; the agent narrows.
Bloc / Cubit example #
inkpalRunApp(
const MyApp(),
stateAdapter: InkPalStateAdapter(
list: () async => [
{'id': 'auth', 'kind': 'Cubit<AuthState>'},
{'id': 'counter', 'kind': 'Bloc<CounterEvent, int>'},
],
read: (id) async {
final bloc = GetIt.I<BlocBase>(instanceName: id);
return {'value': bloc.state.toString()};
},
write: (id, value) async {
if (id == 'counter') {
GetIt.I<CounterBloc>().add(CounterSet(value as int));
return {'success': true};
}
return {'success': false, 'error': 'not writable: \$id'};
},
),
);
Provider (ChangeNotifier) example #
inkpalRunApp(
const MyApp(),
stateAdapter: InkPalStateAdapter(
list: () async => [
{'id': 'theme', 'kind': 'ChangeNotifier<ThemeModel>'},
{'id': 'cart', 'kind': 'ChangeNotifier<CartModel>'},
],
read: (id) async => switch (id) {
'theme' => {'value': themeNotifier.mode.name},
'cart' => {'value': cartNotifier.items.length},
_ => {'error': 'unknown provider: \$id'},
},
),
);
Compatibility #
- 27/27
mcp_server_test.dartpass. 6.0 baseline preserved verbatim. No shape change for any pre-6.6 tool.
Tool count: 60 → 62. #
6.5.0 #
Fifth step on the ladder. Opens a live window into the app's state tree — the piece the 6.0 → ADGP field report kept working around by diffing screenshots and stringifying state through app extensions.
New #
InkPalStateAdapter— a zero-dep, duck-typed struct with four optional callbacks (list,read,write,invalidate). Wire it to your RiverpodProviderContainer/ Bloc registry / Provider tree / GetX bindings / MobX stores. The bridge does not import your state library — you decide exactly what is reachable.inkpal_state_providers— enumerate live providers currently exposed by the app.inkpal_state_read— read the current value of one provider by id. Complementsstate_capture(snapshot journal) with a real-time peek.inkpal_state_override— override a provider's value. Great for agent-driven test setups: seed a logged-in user, pin a locale, flip a feature flag.inkpal_state_invalidate— invalidate / refresh a provider so its next read recomputes. Useful for FutureProvider / StreamProvider.
Wiring #
inkpalRunApp(
const MyApp(),
stateAdapter: InkPalStateAdapter(
list: () async => [
{'id': 'counter', 'kind': 'StateProvider<int>'},
{'id': 'user', 'kind': 'FutureProvider<User>'},
],
read: (id) async => switch (id) {
'counter' => {'value': container.read(counterProvider)},
'user' => {'value': container.read(userProvider).toString()},
_ => {'error': 'unknown provider: \$id'},
},
write: (id, value) async {
if (id == 'counter') {
container.read(counterProvider.notifier).state = value as int;
return {'success': true};
}
return {'success': false, 'error': 'not writable: \$id'};
},
invalidate: (id) async {
if (id == 'user') {
container.invalidate(userProvider);
return {'success': true};
}
return {'success': false, 'error': 'not invalidatable: \$id'};
},
),
);
Compatibility #
- All four state adapter callbacks are individually nullable. Every
state_*handler returns a{success: false, error: 'not configured'}envelope when the app hasn't opted in, so tools stay safe to call unconditionally from an editor. - 22/22
mcp_server_test.dartstill pass. No behaviour change for apps that omitstateAdapter.
Tool count: 56 → 60. #
6.4.0 #
Fourth step on the ladder. Automates the manual missing-link audit the 6.0 field report ran by hand.
New #
inkpal_open_deeplink— drive a deep link into the running app. Delegates through the normal navigation chain (router.go / router.push for go_router, then onNavigateToRoute, then Navigator.pushNamed) and reports resolved route, stack, and before/after. Accepts full URIs or bare route names.inkpal_get_route_graph— declared routes (from go_router configuration + inkpalRunApp knownRoutes) vs. visited routes (observer stack + external navigations). Surfacesorphanedentries — declared routes nothing navigated to.inkpal_coverage_report— session coverage: routes declared vs. visited, plus the tappable elements on the current screen.
Internals #
_appRouterfield added toInkPalBridgeso handlers registered inside_registerCommandscan reach the app router (the localroutername in that scope is the CommandRouter).
Verification #
- 22/22
mcp_server_test.dartstill pass. Compatibility contract holds.
6.3.0 #
Third step on the phased ladder. Answers "what did the app talk to?" in one round-trip.
New #
inkpal_get_http_log— every observed HTTP request (method, URL, status, duration, sizes, error, redacted headers). Filter bysince_ms,url_pattern(regex),method; cap the tail withmax. Sensitive headers (Authorization, Cookie, X-Api-Key, X-Auth-Token, Proxy-Authorization, Set-Cookie) are stripped before storage — the redaction ran in 6.0 already, this exposes it.inkpal_assert_no_failed_requests—{ok, failed_count, failures[]}for any request sincesince_msthat returned>= 400or threw a transport error. Use after any action that should have succeeded network-wise.inkpal_wait_for_request— waits until a request whose URL matchesurl_patternis observed, ortimeout_mselapses. Optionalmethodfilter. Great for "tap BOOK NOW, confirm the POST fired and returned 200."
Verification #
- 6.2 verification-envelope tests plus the 22 pre-6.3 tests still pass verbatim. Compatibility contract holds.
6.2.0 #
Second step on the 6.1 → 7.0 ladder. Backward-compatible with 6.0
and 6.1: every existing response keeps its top-level fields; the new
verification object is additive.
New #
verificationenvelope on every mutating action. Same shape as the tapscreenChanged/addedLabels/removedLabelspattern from 6.0, extended toscroll,enter_text,navigate_to_route,navigate_back,hot_reload,hot_restart,long_press, andcall_app_extension. Each action response now includes:
Agents get "did my action work" in one round-trip, without a follow-up screenshot to compare."verification": { "frame_stamp": 12345, "screen_changed": true, "added_labels": ["Settings"], "removed_labels": ["Home"], "route_before": "/home", "route_after": "/settings", "route_changed": true, "errors_since_action": 0, "since_ms": 1720000000000, "now_ms": 1720000000300 }
Verification #
- Compatibility contract holds:
mcp_server_test.dartstill 22/22. Existing 6.0/6.1 top-level response keys unchanged. dart analyze lib/clean.
6.1.0 #
Field-fix release — first step on the 6.1 → 6.9 → 7.0 phased ladder.
Every fix is backward-compatible with 6.0: no inkpalRunApp parameter
removed, no rename without alias, no response-shape change that
removes existing fields. All 6.0 integration-test assertions still
pass verbatim.
Fixed (from the 6.0 ADGP field report §1) #
- Deep-tree tap — removed the arbitrary 12-level tappable-ancestor
cap. Real design systems bury
GestureDetectorunderContainer→Padding→DecoratedBox→Stack→Positioned→AnimatedContainer… The walk now goes to the semantics root, so aBrandCard(onTap: ...)14 layers deep taps correctly. - Hot-reload lifecycle — rewrote
HotReloadDriveraround a single- listener-per-socket + shared response map. The old "Stream has already been listened to" error after any failed reload is gone; hot-reload can be retried indefinitely without app restart. - Zone-safe bootstrap —
WidgetsFlutterBinding.ensureInitialized()is now called before any zone dance. If the caller wrapsmain()inrunZonedGuarded(...)(Sentry, Bugsnag, custom error reporters),inkpalRunAppruns inside that already-installed error zone instead of nesting a new one. Eliminates the "Zone mismatch" warning surfaced by the ADGP field report. - Silent route-wire gap — when neither
router:,navigatorKey:, noronNavigateToRoute:is passed ANDInkPalNavigatorObserverisn't attached toMaterialApp.navigatorObservers, the bridge now prints a loud one-line warning ~750 ms after boot with the exact one-line fix. Two days silently lost toroute: null— never again. - Hot-reload Android hint —
inkpal_reload_statusandinkpal_hot_reloadfailure envelopes now mentionadb reverse tcp:PORT tcp:PORTfor Android callers who can't reach the VM Service from the host.
New #
reveal:param oninkpal_scroll—reveal: "below" | "above" | "left" | "right"names what should come into view (the intuitive frame). The legacydirection:param stays accepted for 6.0 callers; when both are supplied,direction:wins so 6.0 semantics are preserved verbatim.- Frame stamps on inspection responses — every response from
inkpal_observe,inkpal_get_screen_content,inkpal_get_widget_tree,inkpal_find_widget, plus every tap response, includesframe_stamp(Flutter'scurrentFrameTimeStampin microseconds). expect_frame_stampguard oninkpal_tap— pass the frame stamp from a prior inspection; the tap refuses to fire withframe_stamp_mismatchwhen the frame has moved on. Catches stale- bounds races after scroll animations.
Migration #
None required. 5.x and 6.0.x code compiles and behaves identically. Previously-broken paths now succeed.
Verification #
- Compatibility contract: every integration-test assertion that
passed in 6.0.x still passes verbatim in 6.1 — the 22-test
mcp_server_test.dartsuite is a superset of 6.0's 18 tests. dart analyze lib/clean;flutter test97 pass / 4 skip / 4 fail (same 4 pre-existing test-isolation failures).
6.0.0 #
Field-validated stable release. Fixes every real-world break surfaced by a day of driving 5.0.0 against a live app, adds hot reload / hot restart so the bridge closes the iterate loop, and clears the last pub.dev score gap.
Fixed #
inkpal_tapno longer crashes with "type 'Null' is not a subtype of String". The bridge WS handler assumedparams['label']was a non-null string, but MCPinkpal_tapsendstext/key/x/y. 6.0.0 adds a per-tool argument adapter inInkPalMcpTools.adaptArgs(...)that translates MCP names to bridge names before dispatch, plus defensive nullable casts on every WS handler (inkpal_tap,inkpal_enter_text,inkpal_scroll,inkpal_navigate_to_route,inkpal_long_press,inkpal_tap_with_context,inkpal_increase_value,inkpal_decrease_value,inkpal_get_screen_manifest). Field regression: a whole day was lost toadb input tapfallback because of this crash.inkpal_tap({x, y})now dispatches to the pointer pipeline instead of returning "not found" for an empty label. Raw-coord taps and semantic finders both work through the same MCP tool.- Every WS handler returns a structured error envelope on missing args instead of throwing an uncaught cast exception. The MCP client gets a JSON-RPC result telling it what to fix.
- pubspec description shortened to 167 chars to fit pub.dev's 60–180 window. Recovers the "Provide a valid pubspec.yaml" score.
New #
inkpal_hot_reload— trigger a Flutter hot reload from your AI assistant, no externalflutterCLI needed. The bridge opens a WebSocket to its own Dart VM Service (available in every debug build) and callsreloadSources. Closes the iterate loop:edit code → inkpal_hot_reload → inkpal_wait_for_idle → inkpal_assert_no_errors → inkpal_screenshot.inkpal_hot_restart— full state-clearing restart.inkpal_reload_status— check whether the VM Service transport is reachable from the running app before attempting a reload.inkpal_scrollnow acceptspixelsfor exact scroll deltas.
Improved #
- Scroll semantics documented in the schema.
direction: downreveals items BELOW the current viewport (the finger swipes UP). Prior 5.x users burned cycles on this — the schema description now spells it out. - Welcome banner detects Android and prints the exact
adb forward tcp:8767 tcp:8767command every app boot needs. No more losing a session to a missing forward. - Regression tests cover every parameter-adapter rule. Tap null
crash, raw-coord tap,
route→routeName,text→label, nestedcall_app_extensionparams — all locked in.
Coverage #
- 56 MCP tools (was 53 in 5.0.0). The three new hot-reload primitives are additive.
Migration #
None required. 5.x code works identically; broken 5.x paths now succeed.
5.0.0 #
The final working version — one dependency, one line, one JSON block, done.
inkpal_bridge is now the whole product. Add the package, wrap runApp,
run the app, and every AI coding assistant that speaks MCP can drive it
over http://127.0.0.1:8767/mcp. No CLI to install. No npm proxy. No
Node. No signup. No API key. No license.
What's new #
- Full tool surface — the in-process MCP HTTP server now exposes every meaningful command the bridge already serves internally: 53 tools covering inspection (observe, get_widget_tree, find_widget, screen_snapshot, screen_diff, device_metrics, screenshot), interaction (tap, long_press, scroll, enter_text, navigate, increase/decrease_value, touch feedback), wait/assert primitives, accessibility audit, translation coverage, structured log query, evaluate, app-extension state seed, state time-travel (capture / list / get / diff / stream), interaction recording, and self-healing (watch / verify / error context).
- Every tool goes through the same alias-aware dispatch
(
InkPalMcpTools.commandFor), so public MCP names likeinkpal_taproute to their internal command (tap_element) regardless of how the two evolve. - Every tool is regression-guarded — the integration test invokes each of the 53 tools over real HTTP and asserts a routable bridge command exists. Alias drift now fails CI, not fresh installs.
Simplified setup #
# pubspec.yaml
dependencies:
inkpal_bridge: ^5.0.0
// lib/main.dart
void main() => inkpalRunApp(const MyApp());
// your MCP client's config (Claude Code / Cursor / Codex / …)
{"mcpServers":{"inkpal":{"transport":"http","url":"http://127.0.0.1:8767/mcp"}}}
That's the entire install. No other component.
Notes #
- No public API removals since 4.1.1. The
licenseKey:andapiUrl:deprecated shims from 4.0 stay for one more major so 3.x callers keep compiling. - The bridge still binds to loopback only (
127.0.0.1), debug-mode only, port-in-use degrades to WS-only. Same safety envelope.
4.1.1 #
Bugfix release. The MCP HTTP transport shipped in 4.1.0 dispatched
tools/call by stripping the inkpal_ prefix from the tool name, but
several public tool names don't map 1:1 to the bridge's internal WS
command names — so calls like inkpal_tap, inkpal_screenshot, and
inkpal_get_interactive_elements returned "Method not found" instead
of running.
- Fixed:
InkPalMcpTools.commandFor(...)now applies an explicit alias table before falling back to the prefix strip:inkpal_tap→tap_element,inkpal_screenshot→take_screenshot,inkpal_get_interactive_elements→get_screen_content,inkpal_get_runtime_errors→get_error_history,inkpal_get_app_logs→get_log_history,inkpal_navigate_back→go_back,inkpal_enter_text→set_text,inkpal_list_app_extensions→app_list,inkpal_call_app_extension→app_call,inkpal_long_press→long_press. - Added: 12-test integration suite (
test/mcp_server_test.dart) that spins up a realHttpServer, sends actual JSON-RPC requests, and asserts the full MCP protocol: initialize handshake, notifications, tools/list registry contents, tools/call dispatch (both aliased and prefix-stripped forms), unknown-tool + unknown- method errors, malformed-JSON parse error, CORS preflight, 404s on non-/mcppaths, and clean port release on stop.
4.1.0 #
No CLI install required. The bridge now serves MCP-over-HTTP from
inside the running app, so AI editors can connect to it directly — no
npx, no Node, no separate process.
New #
- In-process MCP server. Defaults to
http://127.0.0.1:8767/mcp. Editors that support the MCP HTTP transport (Claude Code, Cursor, Windsurf, VS Code Copilot, Codex CLI) connect directly with a single JSON paste into their MCP config — no install of anything besides the Dart package itself. mcpPort:parameter oninkpalRunAppandInkPalBridge.init. Defaults to8767; pass0to disable, or another port to avoid collisions in parallel-run setups.InkPalMcpServer— anHttpServerbound to127.0.0.1, JSON-RPC 2.0 over POST plus an SSE keep-alive endpoint. Implementsinitialize,tools/list,tools/callagainst a bundled registry of ~30 bridge-direct tools that map onto the sameCommandRouterthe WebSocket transport uses.- Welcome banner prints both transports + the JSON snippet to paste into an editor's MCP config.
Improved #
- The two transports coexist. The bridge keeps its outbound WebSocket
(legacy
npx inkpal startpath) for users who want the full ~150-tool surface that includes cloud catalogs, Figma, and host CLI tools — those still require the npm proxy. Bridge-direct tools (~30) work over either transport with the same handler code.
Notes #
- The MCP server binds to loopback only (
127.0.0.1), permissive CORS only on that interface. No traffic leaves the machine. - Disabled in release mode — same
kDebugModegate as the rest of the bridge. Release builds collapseinkpalRunApptorunAppwith zero overhead, including the MCP listener. - Failure to bind the MCP port (port-in-use, sandboxed runtime) is non-fatal — the WebSocket path continues to work and the bridge logs a warning instead of crashing.
4.0.1 #
README-only update. No code changes.
- Slim the README to fit the free-package framing: drop the marketing walkthrough, the "what you get" subsections, and the signup-flavoured documentation links.
- Add a manual MCP-client config block so adopters without Node have an
install path that doesn't need
npx. - Welcome banner no longer mentions a parallel
inkpal.ai/setupURL.
4.0.0 #
Free for everyone — and 10 new bridge-side tools. No signup, no API key, no tier check. The bridge runs unconditionally in debug mode.
Migrating from 3.x #
No source changes required. Existing call sites keep compiling. The
licenseKey: and apiUrl: parameters are accepted as deprecated no-ops
and you'll see a yellow analyzer hint plus a one-line debug log at boot:
[InkPal] DEPRECATED: inkpalRunApp(licenseKey: ...) is ignored —
inkpal_bridge is free for everyone in 4.0. Remove the parameter.
To silence both, delete the two parameters:
// 3.x
inkpalRunApp(
MyApp(),
licenseKey: const String.fromEnvironment('INKPAL_LICENSE_KEY'),
apiUrl: 'https://mcp.inkpal.ai',
);
// 4.0
inkpalRunApp(MyApp());
If you were running with flutter run --dart-define=INKPAL_LICENSE_KEY=...,
drop the --dart-define — the env var is no longer read.
The deprecated parameters will be removed in 5.0.
Removed #
InkPalLicenseValidator,FeatureGate,FeatureTier,InkPalFeature,InkPalTier,InkPalLicense,InkPalBridge.licenseReady. These were the public API of the license layer. If you imported any of them directly, they're gone; if you only usedinkpalRunApp, you won't notice.
New #
assert_no_errors— count + samples ofFlutterErrors captured since a timestamp. Cheap to call after every gesture in a test loop.stability_check— fused{errors, jank_frames, state_churn, ok}so one round-trip answers "is the app behaving right now."assert_elementandget_elements— read-side assertions and typed queries (type/key_prefix/text_contains/tappable_only) with bounds + tappability. Replaces "screenshot then ask the LLM to look."wait_forandwait_for_idle— poll until a predicate matches or the app settles for N consecutive frames. Replaces "tap → screenshot → check → repeat" loops with a single call.accessibility_audit— code-only WCAG checks (touch target ≥48dp, missing labels on interactive elements, image alt text). Runs entirely in-app, no cloud catalog.query_logs— filter the in-app log buffer by level, category, time, and regex pattern. Avoids streaming the full buffer over the wire.evaluate— Dart expression evaluator via an opt-in hook the app registers. Returns the JSON-encoded result. Apps decide which symbols are reachable; off by default.verify_translations— locale key coverage report via an opt-in hook. Catches forgotten translations in CI.
Improved #
- Welcome banner rewritten — the bridge now identifies itself simply as "free for everyone, no API key, no signup" instead of the old license-state line.
3.0.0 #
The AI-native release. New capabilities for AI-assisted development — and no removed or changed public APIs: every 1.x / 2.x export still imports and compiles.
New #
observe()— one-call situational awareness. A single snapshot of the current route, navigation stack, interactive elements, a short text summary, recent logs and errors, and your live app state (optional screenshot). An assistant can ground its next action in one call instead of many reads.- App extensions —
InkPalAppExtensions.register(...). Expose app-specific operations (reset/seed data, flip a feature flag, jump onboarding) that an assistant can invoke directly, without shipping a new bridge version. Discoverable and callable from the MCP tools. router:oninkpalRunApp. Pass yourGoRouter(or any router exposing.go/.push) and the bridge drives named navigation. For go_router it also tracks the current route automatically — including imperativecontext.push— with noNavigatorObserverwiring.- Device metrics. Read the logical viewport size, device pixel ratio, safe-area padding, and orientation so coordinate gestures use the right units.
Improved #
- Keyed taps are reliable. Tapping by
ValueKeynow resolves the widget in the element tree and taps its center through the real gesture pipeline —IconButton,FloatingActionButton, and deeply nested buttons that previously couldn't be found now tap correctly. The tappable-ancestor search also reaches deeper widget nesting. - Richer tap results. Taps report whether the tap landed, plus the labels that appeared and disappeared, so the outcome is unambiguous.
- Faster hot reload through the Flutter daemon, with compile errors surfaced directly.
- Honest gesture inputs. Swipe and drag honor explicit coordinates and report an error on partial input instead of guessing; scroll falls back to a coordinate drag when no scrollable area is detected.
Fixed #
- Text entry into a missing field now reports failure instead of silently writing to whatever was focused and returning success. The error lists the text fields actually on screen so the caller can retry against the right one.
2.0.0 #
- Showcase milestone — no public API removals or signature changes. Every export from 1.x still imports and compiles. The one behavioural default flip is called out below ("opt-in error overlay").
- Opt-in error overlay.
inkpalRunApp(enableErrorBoundary: ...)now defaults tofalse. The in-app banner that previously rendered above every screen sat outsideMaterialApp, so it had no Directionality / Overlay ancestor and could cascade into hit-test failures on apps usingCustomScrollView/Stack-heavy layouts. The error CATCHER (which feedsinkpal_get_runtime_errorsand the bridge's error stream) is a separate subsystem and keeps running regardless of the flag. PassenableErrorBoundary: trueto restore the previous banner. - Semantics walker stability.
captureScreenContextno longer dispose-and-re-acquires the semantics handle on every call. The always-re-acquire pattern triggered Flutter'sdebugFrameWasSentToEngineassertion when the capture ran outside a build phase (typical for VM-service callbacks). Re-acquire is now only triggered when the handle is missing. - Error overlay layout fix (only relevant if you opt in to it):
uses
Aligninstead ofPositioned(top:0)so the subtree gets finite constraints, wraps in its ownDirectionalitysince the boundary sits aboveMaterialApp, and drops theTooltipon the dismiss button (Tooltip needs anOverlayancestor). - 9-zone example app. The example is now a realistic, multi-zone Flutter app covering Core Debug, Visual Debug, Auto-Fix, Runtime Intel, Error Intel, Visual Testing, Developer Experience, Smart Assist, and Forms — each zone planted with the kind of patterns an AI assistant is expected to detect, explain, and act on against a real running app.
- Smoke test suite covering boot + navigation + grid layout in
example/test/smoke_test.dart. inkpalRunApppackage-level dartdoc clarified — single recommended entry point, withInkPalBridge.initdocumented as the power-user / release-mode path.
1.5.0 #
- Stability milestone.
inkpalRunAppis now the single recommended entry point and exposes the full configuration surface. Power-user flags previously reachable only throughInkPalBridge.init(knownRoutes,routeDescriptions,onNavigateToRoute,walkerHooks) are now first-class parameters oninkpalRunApp. - Example app updated to demonstrate the recommended pattern —
multi-zone showcase (counter / forms / list / custom widgets) booted
with one
inkpalRunApp(...)call, no manual bridge wiring. - Example smoke test added (
example/test/smoke_test.dart) — three widget tests covering boot, counter interaction, and route navigation. - Package-level dartdoc rewritten to lead with the one-line
inkpalRunApp(MyApp())setup.InkPalBridge.initis documented as the power-user / release-mode path. - Version constant synced.
inkpalBridgeVersionis now bumped in lockstep with the pubspec;ext.flutter.inkpal.pingreports the real shipping version. - Manual
InkPalBridge.initand direct exports (InkPalErrorCatcher,InkPalHttpMonitor,InkPalErrorBoundary, etc.) remain fully supported — no breaking changes.
1.4.7 #
inkpalRunAppnow works without any license key. The bridge starts in offline free-tier mode, prints a welcome banner with next-step guidance, and begins watching for errors, navigation, and HTTP traffic immediately.- Added periodic idle diagnostics when no client is connected, plus inline connect / disconnect notices when an AI client attaches.
- Caught errors now surface in a compact console format with location parsed from the stack trace.
- Default API endpoint now resolves through the canonical InkPal host.
- Dropped the unused device-fingerprint provisioning path.
1.4.6 #
defaultApiUrlupdated to the canonical production endpoint.
1.4.5 #
- Pubspec description trimmed to fit pub.dev's 60–180 character window so search snippets render the full description. No code change.
1.4.4 #
- README simplified — single primary use case ("let your AI inspect, debug, and control your running Flutter app"), real-conversation example near the top, three-capability summary (Read / Drive / Catch), trimmed command catalog. The example app already demonstrates the full surface.
- License flow clarified:
inkpal_bridgeis free for personal and commercial use with a key from inkpal.ai signup. Documented as the install path.
1.4.3 #
- README rewrite. Stronger positioning ("give your AI a working set of
hands inside your Flutter app"), explicit comparison vs
marionette_flutterandflutter_driver, simplified quick-start path (1 init call), reordered tier capabilities into use-case columns, added privacy/security section, dropped jargon ("MCP-native"). No code change. - Pubspec description rewritten to lead with capability, not acronym — improves first-touch comprehension on pub.dev search results.
- Custom widgets documented.
InkPalWalkerHooks(already public since 1.4.0) is now first-class in the README + has a working demo inexample/lib/main.dart. Lets the AI agent recognise proprietary widgets (BrandButton,GlassCard, etc.) by label withoutSemantics(label:)wrappers. Closes the recognised gap vsmarionette_flutter. - Example app rewritten as a multi-zone showcase modelled on the
InkPal battlefield app — counter zone, forms zone, list zone,
custom-widgets zone exercising
walkerHooks. - Pub.dev score recovery. Score was 145/160. Two deductions fixed:
(1)
auto_provision.dart:20andlog_buffer.dart:11had<placeholder>tokens in dartdoc that the analyser flagged as HTML. Replaced with shell-style$placeholder. (2) CHANGELOG now lists 1.4.1+1.4.2+1.4.3 in canonical heading format. - Topics updated for discoverability:
mcp,ai,claude,copilot,agent(wasai,mcp,testing,automation,devtools). - Pricing copy aligned with single Pro model. Earlier 1.4.3 draft carried over the legacy Free / Pro / Studio capability table. Replaced with one "What you can do" table — every capability ships with any valid license (trial or paid). Reflects the locked single-Pro pricing model on inkpal.ai.
- First-touch pivot: lead with "start free, full power", not prices.
Dollar/rupee amounts removed from the README — pub.dev visitors are
evaluating, not buying. Pricing details live at inkpal.ai/pricing
for visitors who're already convinced. Same change applied to the
npm
inkpalpackage README. Studies show pricing-on-first-touch is a conversion killer for dev tools when the product hasn't earned attention yet.
1.4.2 #
- Version-sync fix.
inkpalBridgeVersionconstant inlib/src/_version.dart(whichext.flutter.inkpal.pingreports to the MCP server) was stuck at1.4.0while pubspec moved to 1.4.1, causing every handshake to mis-report the running bridge version. - Pub.dev score recovery. 1.4.1 lost 5 score points because its
CHANGELOG.md didn't include a
## 1.4.1heading. This release fixes the doc convention going forward and documents both 1.4.1 + 1.4.2 changes below. - No public API changes from 1.4.1 — drop-in upgrade.
1.4.1 #
- Packaging hygiene. Untracked stale
.dart_tool/artefacts that were leaking into the published archive (3 entries in package root, 2 underexample/). Tarball validation cleaner, no source changes. .pubignoreextended to explicitly excludeexample/.dart_tool/andexample/build/so theexample/showcase app's local build state can never bloat future releases.
1.4.0 #
- App-registered VM extensions. Host apps can expose their own
operations via
InkPalAppExtensions.register(name:, description:, handler:)underext.flutter.inkpal.app.<name>. Enumerated + invoked through the new MCP tools so downstream teams extend InkPal without a bridge republish. - Synthetic pointer driver + hit-test probe. New coordinate-tap
path dispatches
PointerAdded/Down/Up/RemovedthroughGestureBinding.handlePointerEvent, interpolating 40px steps for drags. Works on widgets that never emit aSemanticsAction.tap— rawListener, customGestureRecognizer,CustomPainthit regions. Opt-in viax+yparams onext.flutter.inkpal.tap; semantics stays the default. Includes aprobeHit()helper that reports whether a point can actually reach a target RenderObject or is absorbed by aModalBarrier/IgnorePointer/ overlay first. - Walker hooks for design-system widgets.
InkPalBridge.init(walkerHooks: InkPalWalkerHooks(isInteractiveWidget:, shouldStopTraversal:, extractTextFrom:))lets host apps surface their own widget types to the agent's interaction vocabulary. All three callbacks optional; built-in walker rules still apply when hooks are missing. - Zero-config screenshot. When no
RepaintBoundaryis wired by the host app, falls back to compositing the firstRenderView's live layer tree into a freshSceneand encoding offscreen. Apps that initialise the bridge manually withoutinkpalRunAppnow get working screenshots out of the box. Identical return shape — callers can't tell which path ran. - Adaptive scroll.
scrollToFindnow iterates every on-screen scrollable deepest-first and gives each a full-budget down + up sweep instead of splitting 10/10. Default attempt cap raised to 50; stall detection still breaks the loop early on short lists. Fixes the "scrolling the wrong scrollable" class of miss on nested lists.
1.3.4 #
- Default API host updated.
- Topics: add
automation, dropdebugging.
1.3.3 #
- B5 fix: element walker re-acquires semantics handle per capture.
After multiple push/pop navigation cycles the original
SemanticsHandlecould become detached from the currentPipelineOwner, leaving the semantics tree empty even though widgets render visually. The walker now disposes + re-acquires the handle before eachcaptureScreenContext()call (_reensureSemantics()), and iterates allrenderViewsinstead of only the first. This fixesgetWidgetTree+getScreenContent+tapall returning empty/failing after navigation.
1.3.2 #
- Auto-wired navigator key + observer. Two new globals exported from
the package barrel:
inkpalNavigatorKey—GlobalKey<NavigatorState>the bridge uses to driveNavigator.pop()directly fromext.flutter.inkpal.goBack, no more "tap the back arrow" gymnastics.inkpalNavigatorObserver— singletonInkPalNavigatorObserversogetCurrentRouteworks without consumers having to construct their own observer. Wire on yourMaterialApp:
Without these,MaterialApp( navigatorKey: inkpalNavigatorKey, navigatorObservers: [inkpalNavigatorObserver], home: ..., )goBackfalls back to its old tap-the-arrow behaviour.
1.3.1 #
Field-test bug fixes surfaced by running the bridge against a real Flutter
benchmark app and exercising ext.flutter.inkpal.* extensions.
- B1 — Screenshot extension always failed on freshly launched apps.
inkpalRunAppnow wraps the user's root in aRepaintBoundarytagged with the package-wideinkpalRootRepaintKey.ScreenshotCapturefalls back to that key when the configuredappContentKeyhas no context, and the newcaptureWithDiagnostics()API surfaces the actual exception + truncated stack instead of the opaque"Screenshot capture failed"string. Theext.flutter.inkpal.screenshotextension now reports the real cause of failure. - B2 —
getCurrentRoutereturned null afterMaterialPageRoute(builder:)pushes.InkPalNavigatorObserverpreviously skipped routes whosesettings.namewas null, leaving the stack empty even after a successful push. The observer now falls back to a<RuntimeType>synthetic identifier (e.g.<MaterialPageRoute<void>>). For stable identifiers across builds, callers should still passMaterialPageRoute(builder: ..., settings: RouteSettings(name: '/foo')). - B3 —
getWidgetTreedid not surfaceValueKey<String>-tagged anchors like FigmaScaffold(key: ValueKey('fig-2-5')). The tree walker now runs an Element-tree pass after the semantics pass and emits aUiElementType.keyedentry (with the newkeyfield onUiElement) for everyValueKey<String>-tagged widget — surfacing structural anchors that don't carry a semantics label of their own. - B4 —
ext.flutter.inkpal.pingreported the hardcoded version"1.0.0". Addedlib/src/_version.dart— a singleinkpalBridgeVersionconstant that the ping handler reads. Bump this in lockstep withpubspec.yaml(no codegen, no extra dep).
New tests #
test/screenshot_capture_test.dart— fallback toinkpalRootRepaintKey- structured failure-error coverage.
test/route_observer_test.dart— unnamedMaterialPageRoutepush now populates the route stack; explicitRouteSettings.namestill wins; pop removes the synthetic identifier.test/widget_tree_keyed_test.dart—ValueKey<String>-tagged Scaffold surfaces withtype: keyed; multiple keyed widgets de-dupe; non-string ValueKeys are ignored; JSON round-trip carries the key.
1.3.0 #
- Socket reconnect hardened — exponential backoff with jitter (500ms base × 2^attempt, ±200ms), 5-minute cap, stops after 30 consecutive failures
- VM extensions decoupled from WS state — extensions now work regardless of WebSocket connection status
- Fixed
dispose()LateInit crash — safe to call repeatedly, including after failed init _perfMonitorguaranteed initialized in allinit()code paths- Testing hooks exposed —
@visibleForTestinggetters forrouter(CommandRouter) andsemanticsWalker(SemanticsWalker) SemanticsWalkerre-exported from public barrel- New tests:
reconnect_test.dart,dispose_test.dart,visible_for_testing_test.dart— 12+ new tests across all new/fixed behavior - New test fixture:
test_app/with keyed widget harness + 40 integration tests covering all 33 VM extensions - New CI:
.github/workflows/bridge-e2e.ymlruns on Android emulator + iOS simulator on every bridge push
1.2.2 #
- Remove comparison-to-alternatives section from README
- No code changes
1.2.1 #
- Sharpened pubspec description with target search keywords (Flutter MCP, runtime error capture, HTTP monitor, VM service extensions)
- README: added comparison table vs
moinsen_runappandmarionette_mcp - README: bumped install version to ^1.2.1
- No code changes
1.2.0 #
- Added
InkPalErrorCatcher: three-layer error capture (FlutterError + PlatformDispatcher + runZonedGuarded) with time-windowed deduplication and handler chaining - Added
InkPalHttpMonitor: read-only HTTP request monitor with redacted headers, ring buffer, and collision detection againstInkPalNetworkInterceptor - Added
InkPalErrorBoundary: Stack-based overlay widget that survives app rebuild failures, with debug + release builders - Added
generateInkPalBugReport: packages errors, HTTP, logs, route, and app state as markdown for LLM consumption (capped at 8k chars) - Added
inkpalRunApp: drop-inrunAppreplacement wiring error catcher + HTTP monitor + error boundary + bridge init inside a guarded zone - Added optional
errorCatcherandhttpMonitorparameters toInkPalBridge.init(backward-compatible) - Fixed
ErrorSubscribersilently dropping errors beforestartWatching()— ring buffer now always populated,_watchinggates only stream notification
1.1.0 #
- WebSocket connect timeout (10s) with TimeoutException on miss
licenseReadyfuture on InkPalBridge for license-gated startup flows- Grace period enforcement moved to
tiergetter (callers bypassinghasFeature()now get downgraded correctly) - Screenshot capture wrapped in 5s timeout — returns null instead of hanging
- License validator test suite (validation flow, grace period, throttle, network failure)
1.0.1 #
- Shortened package description for pub.dev display
- Fixed homepage URL (now points to GitHub repo)
- Removed web platform declaration (package uses
dart:io) - Suppressed
hasFlagdeprecation warnings
1.0.0 #
Initial stable release.
Core #
- WebSocket-based bidirectional communication (JSON-RPC 2.0)
- Automatic reconnection with exponential backoff (1s-64s)
- 10-second connection timeout
- Message buffering during disconnects (100 message cap)
- Zero overhead in release builds (bridge is
null)
Inspection #
- Semantics tree walking — read UI without instrumentation
- Widget tree as structured JSON
- Element search by label or text
- Screenshot capture with configurable width (default 720px, 5s timeout)
- Screen context caching with automatic invalidation
Interaction #
- Tap, long press, double tap by label/key/semantics
- Text field input
- Scroll (directional + scroll-to-element)
- Slider/stepper increment and decrement
- Route navigation (push, pop, go)
Navigation #
- Route tracking via NavigatorObserver
- Full navigation stack access
- Route discovery (all routes seen in session)
- Support for standard Navigator, go_router, GetX, and Beamer
- Custom
onNavigateToRoutecallback for any router
Telemetry #
- Structured logging (log, debug, warning, error levels)
- Real-time error streaming with immediate delivery
- Log batching (500ms intervals for non-error logs)
- Error context enrichment (widget tree, state, recent logs)
- Log correlation — time-windowed action-to-log mapping
State Time-Travel #
- State snapshot capture
- Snapshot listing and retrieval
- State diffing between any two snapshots
- Live state stream observation
- Max 50 snapshots with automatic eviction
Interaction Recording #
- Record user/AI interactions as typed actions
- Export as JSON or Dart integration test code
- Recording status monitoring
Layout Diffing #
- Screen layout snapshot capture
- Before/after layout comparison
Performance #
- FPS monitoring
- Jank detection
Network Control (Studio tier) #
- Per-URL HTTP mock rules (pattern matching, response code, body, delay)
- Offline mode (block all requests)
- Latency and packet loss simulation
VM Service Extensions #
- 33 extensions registered under
ext.flutter.inkpal.* - Fallback channel for local development without WebSocket
- Mirrors all WebSocket commands
License Gating #
- Three tiers: Free, Pro, Studio
- Server-side validation with signed grants (HMAC-SHA256)
- 24-hour grant TTL with 7-day grace period
- Validation throttling (60s cooldown)
licenseReadyfuture for callers needing gate sync
App Manifest #
- Rich
AiAppManifestfor LLM context - Screen manifest with widget descriptions
- App map with full structure
Touch Visualization #
- Visual ripple overlay for AI-driven interactions
- Configurable via
TouchVisualizerController
Platform #
- Zero third-party dependencies (Flutter SDK only)
- Flutter 3.10+ / Dart 3.0+
- MIT license