fluttersdk_dusk 0.0.8
fluttersdk_dusk: ^0.0.8 copied to clipboard
Flutter E2E driver for LLM agents and CI. 34 CLI commands and 33 MCP tools drive a running app over VM Service extensions; no flutter_test harness needed.
Changelog #
All notable changes to this project will be documented in this file.
This project follows Semantic Versioning 2.0.0. Entries follow the Keep a Changelog shape.
Unreleased #
0.0.8 - 2026-06-17 #
Changed #
dusk:installnow injectsimport 'package:magic_devtools/dusk.dart';and gates on themagic_devtoolsdependency instead of the removedpackage:magic/dusk_integration.dart. TheMagicDuskIntegrationclass was extracted from themagiccore into the newmagic_devtoolspackage; the injected class name (MagicDuskIntegration.install()) is unchanged. Consumers that follow magic's install.yaml (which addsmagic_devtoolsto dev_dependencies before runningdusk:install) get the integration wired automatically; magic-only consumers withoutmagic_devtoolsin pubspec.yaml are unaffected. Coordinated with the magic_devtools extraction.
Fixed #
dusk:installno longer injectsimport 'package:magic_devtools/dusk.dart';orMagicDuskIntegration.install()into a vanilla Flutter app that hasmagic_devtoolsin its pubspec but noawait Magic.init(call inlib/main.dart. Previously, themagic_devtoolswiring block ran whenever the pubspec listed the dependency, regardless of whether aMagic.initanchor existed. This left an unused import in the consumer's file, causingdart analyzeto fail. The gate is nowhasMagicInit && _hasMagicDevtoolsDep(), matching the block's own intent documented in the comment above it. The existingtry/catcharoundinjectAfterMagicInitis retained as a defensive fallback.
Documentation #
- Docs, skill, and example synced to the
magic_devtoolsextraction:doc/plugins/magic-integration.mdupdated to note thatMagicDuskIntegrationnow ships inmagic_devtools(add as a dev_dependency) and shows the requiredimport 'package:magic_devtools/dusk.dart';.skills/fluttersdk-dusk/references/cli-commands.mdupdated to reflect themagic_devtoolsgate andmagic_devtools/dusk.dartimport.ARCHITECTURE.mdfrozen-contracts item updated frommagictomagic_devtools. Version pins bumped to^0.0.8throughout (pubspec.yaml,example/pubspec.yaml,doc/getting-started/installation.md,skills/fluttersdk-dusk/SKILL.md).
0.0.7 - 2026-06-17 #
Added #
-
dusk:consolenow surfacesdebugPrintoutput even withoutfluttersdk_telescope.DuskPlugin.install()now chains adebugPrintoverride that records every call into a bounded in-package ring buffer (cap 50, newest-first).ext.dusk.consolemerges this buffer with the existing telescoperecentLogsReaderoutput using the same merge+dedup pattern asext.dusk.exceptions, sodebugPrint(...)/print(...)calls appear indusk:consoleresults regardless of whether telescope is installed. The telescope reader indirection is preserved: when telescope is wired it augments withLogger.root.onRecordentries and any other watchers it ships. Directdart:developerlog()calls that bypassdebugPrintare not captured by the in-package path; they require telescope'sLogWatcher. Capture scope is documented indoc/commands/index.mdunder "Console and exceptions". -
Opt-in
verifyflag ondusk:tap/dusk_tap/ext.dusk.tap. Whenverify: true, the tap handler captures a cheap TARGET-scoped signal before and after the pointer (the nearest enclosing route name plus a hash of the target element's own semantics subtree: label, value, enabled/checked flags) and adds achanged: true|falsefield to the response reporting whether the tap produced an observable effect on the target. The signal is deliberately target-scoped, not a global route/whole-tree hash, so a counter button whose own label increments reportschanged: truewhile unrelated background churn elsewhere in the tree does not. Default off (verify: false) keeps the response shape byte-identical to before: nochangedkey. Thedusk:tapCLI gains a--verifyflag and thedusk_tapMCP descriptor gains averifyboolean property (a parameter addition to the existingdusk:tap/dusk_tap, not a new command or tool). -
Optional
sincefilter ondusk:exceptions/dusk_exceptions/ext.dusk.exceptions. Passsince: "<iso8601>"(e.g.2024-01-01T10:00:00.000Z) to receive only exceptions whosetimeis strictly after that timestamp. Agents can record the current time before an action, then calldusk:exceptions --since=<time>afterwards to see only new exceptions raised by that action, eliminating false positives from cumulative history. Default behavior (nosince) is unchanged: the full cumulative list is returned. Unparseablesincevalues are silently treated as absent. Thedusk:exceptionsCLI gains a--sinceflag and thedusk_exceptionsMCP descriptor gains asincestring property (a parameter addition to the existingdusk:exceptions/dusk_exceptions, not a new command or tool). -
dusk:fill/dusk_fill/ext.dusk.fill— one-call text-field fill. Resolves a text-field ref (e<N>/q<N>), then focuses, clears, types, and settles in a single round-trip, replacing the manual focus + clear + type + settle dance every agent re-discovers. Composes the existing GATEDext.dusk.focus,ext.dusk.clear, andext.dusk.typehandlers verbatim (so the 6-check actionability gate, IME focus,onChanged/validator firing, and post-action snapshot semantics are reused, never re-implemented). Retries the whole resolve + focus + clear + type sequence ONCE when the ref goes stale mid-fill (a transiently-missingq<N>re-walks the now-settled tree on the second pass); a second stale outcome surfaces a typedstaleenvelope so the agent re-snaps or re-finds. Returns{ref, text, filled: true}plus an optional post-fill snapshot. This is a NEW command (CLI 32 -> 34) and a NEW MCP tool (31 -> 33) backed by a NEWext.dusk.fillextension (28 -> 30). -
dusk:reset_overlays/dusk_reset_overlays/ext.dusk.reset_overlays— one-call overlay reset. Returns the app to a known clean screen via three escalating, idempotent layers: (1) pop everyPopupRoute(reusingdismissAllModals, never touching the page stack); (2) anEscapekey press for overlays driven by the dismiss shortcut that are notPopupRoutes; (3) a Cancel/Dismiss/Close/OK/Done labelled tap for modal barriers that need an explicit affordance. Each layer is a no-op when the prior already cleared the overlays, so the command is safe to call speculatively between flows. Returns{popped: N, escaped: bool, dismissTapped: bool}. This is a NEW command (CLI 32 -> 34) and a NEW MCP tool (31 -> 33) backed by a NEWext.dusk.reset_overlaysextension (28 -> 30). -
untilconfirmation ondusk:tap/dusk_tap/ext.dusk.tap. Whenuntil: "<text>"is set, after the tap settles the handler polls the live element tree (reusing thedusk:wait_forpoll loop) for aTextwhose data equals the expected string, up tountilTimeoutMs(default 3000), and adds anuntilMatched: true|falsefield reporting whether it appeared. Confirms a navigation / state change produced the expected text in one call, replacing a separatedusk_wait_forround-trip. Default off (nountil) keeps the response shape unchanged. Thedusk:tapCLI gains a--untilflag and thedusk_tapMCP descriptor gainsuntil/untilTimeoutMsproperties (a param addition, not a new tool). -
"Driving real apps: gotchas for agents" doc page (
doc/getting-started/driving-real-apps-gotchas.md). Captures the hard-won lessons from a long real-app E2E session, each now partly or fully addressed by D1-D7: refs go stale on rebuild (re-snap, or prefer aq<N>fromdusk:find); text fields may snapshot nested (usedusk:fill, or note thetypeable: truemarker on the collapsed outer node);dusk:consolecapturesdebugPrintin-package now and is enriched by telescope;dusk:exceptionsis cumulative (use--since);restartpreserves the CDP port; overlays may needdusk:reset_overlays. Linked fromllms.txt.
Fixed #
dusk:dismiss_modalsnow dismisses modals on ALL NavigatorState instances, not just the first. The previous implementation walked the element tree with a first-match guard and poppedPopupRouteentries one-at-a-time with anendOfFrameawait between each pop.showDialogdefaults touseRootNavigator: true(root navigator) andshowModalBottomSheetdefaults touseRootNavigator: false(nearest navigator); when these are different navigators, the first-match walk left one modal open. The fix collects everyNavigatorStatein a full DFS walk, then callspopUntil((r) => r is! PopupRoute)on each navigator innermost-first, counting everyPopupRoutedismissed. Thepoppedreturn value is the additive sum across all navigators. The per-popendOfFrameawait is removed, which also unblocks unit tests that previously hung the flutter_test fake-clock harness when real modal routes were open.
Changed #
- Pointer verbs now dispatch at the element's LIVE rect, not the cached snapshot rect. Every pointer verb (
tap,hover,dragstart + end endpoints,dblclick,right_click,triple_click) re-resolves the target's current bounding rect via the newdispatchRectOf(entry)helper immediately after the actionability gate passes and dispatches at that live center, falling back to the cachedentry.rect.centeronly when the live rect is null (sliver / detached / synthetic). A host that rebuilt the target into a shifted slot between snapshot and action retains the sameElement/RenderObjectidentity, so the live rect is valid. This fixes the false-success class wheredusk:tapreported success whileonTapnever fired because the pointer landed on the target's stale gate-time position. The helper is purely additive to the FROZEN actionability gate: it runs after the gate passes and before dispatch, touching neither the gate order nor any failure-reason substring. dusk:snapcollapses nestedtextboxnodes and marks the survivortypeable: true. A windWInputwraps asSemantics(textField:true) > MergeSemantics > TextField; becauseRenderEditableunconditionally owns its owntextFieldSemantics node (flutter#26336) andMergeSemanticscannot absorb it (flutter#160281), the tree carried TWO nestedtextboxnodes and minted twoeNrefs. Agents naturally targeted the inner leaf, wheredusk:typethrew-32000. The snapshot walk now suppresses anytextboxnode whose render object is a render-tree DESCENDANT of an enclosingtextboxnode's render object, emitting a single ref for the outer typeable node so existing scripts keep resolving. The surviving textbox line gains an additivetypeable: truesub-line. Collapse is by render-object CONTAINMENT only, never label/value equality, so two sibling fields sharing a label stay two distinct refs. Thetextboxrole string is unchanged;eNminting stays snapshot-only. The source-side fix lives in wind (W1); this is the defensive dusk-side collapse.- Bumped
fluttersdk_artisanto^0.0.8. Picks up the substraterestartfix that preserves--cdp-portacross the stop/start cycle (sodusk:resize/dusk:devicekeep working afterfsa restart) and the published-config import-path fix in the plugin installer.
0.0.6 - 2026-06-09 #
Added #
dusk:screenshotweb CDP fallback viaPage.captureScreenshot. When~/.artisan/state.jsoncarries acdpPort(a web target), the CLI command sendsPage.enable+Page.captureScreenshot(format,quality,fromSurface: true) over the Chrome DevTools Protocol and writes the decoded bytes directly, bypassing the in-isolateext.dusk.screenshotextension that hangs under CanvasKit+DWDS (issue #13). Native targets (nocdpPort) keep usingext.dusk.screenshot. The command captures the full app frame. This CDP fallback is CLI-only; thedusk_screenshotMCP tool still dispatchesext.dusk.screenshotin-isolate, so web agents should use the CLI for screenshots. Region (ref/rect) capture remains deferred.- Non-fatal
FlutterErrorcapture surfaced bydusk:exceptions.DuskPlugin.install()now chains aFlutterError.onErrorhandler that records every non-fatal error (including RenderFlex overflow, taggedtype: "overflow") into a bounded in-package ring buffer (cap 50, dedup bymessage + stackHead, newest-first).ext.dusk.exceptionsmerges this buffer with the existing telescope reader output, so overflow and other non-fatal rendering errors appear indusk:exceptionsresults even whenfluttersdk_telescopeis absent (issue #14). - Per-ref
overflow:annotation indusk:snapoutput. Interactive nodes inside a currently-overflowing render ancestor now carry an additiveoverflow: truesub-line in the snapshot YAML. The check is a liverenderObject.toStringShort().contains(' OVERFLOWING')call (the Flutter debug-mode convention fromRenderFlex.toStringShort); no retained state, no Expando. Non-overflowing layouts produce no annotation. The annotation silently drops if a future Flutter version renames the suffix;dusk:exceptionsremains the authoritative overflow signal.
Changed #
fluttersdk_artisanconstraint bumped from^0.0.6to^0.0.7(Dart pre-1.0 caret rule:^0.0.7resolves to>=0.0.7 <0.0.8). Consumers now pull in artisan 0.0.7 which hardensstart --cdp-portwith busy-port fast-fail and Chrome/FIFO/profile cleanup (issue #25). All dusk-consumed artisan surfaces (CommandBoot, ArtisanCommand, ArtisanContext.callExtension, McpToolDescriptor, registerExtensionIdempotent, StateFile.read/write) are signature-identical to 0.0.6; the bump is non-breaking.
0.0.5 - 2026-05-28 #
Changed #
fluttersdk_artisanconstraint bumped from^0.0.5to^0.0.6(Dart pre-1.0 caret rule:^0.0.6resolves to>=0.0.6 <0.0.7). Consumers now pull in artisan 0.0.6 which ships the substratemcp:install --invocation=<exec>flag this release depends on for the fallback behavior below.mcp:installfallback whenbin/fsais absent now writesdart run fluttersdk_dusk mcp:serve. The dusk wrapper auto-injects--invocation=fluttersdk_duskwhen forwardingmcp:installto the substrate, so the substrate's.mcp.jsonwriter picks the plugin-aware payload instead of the legacydart run :dispatcher mcp:servefallback. No change in behavior when fastcli is present; the./bin/fsa mcp:servepayload is unchanged.- Renamed every dart run fluttersdk_artisan reference inside the dusk package to dart run fluttersdk_dusk (33 docs/code occurrences). The dusk wrapper proxies the full artisan command surface; the package-local invocation is now canonical inside dusk's own docs, error messages, dartdocs, and chained subprocess calls. Substrate package:fluttersdk_artisan/ Dart imports unchanged.
Fixed #
bin/fluttersdk_dusk.dartnow forcescollectMcpTools: truewhen dispatchingmcp:serve, sodart run fluttersdk_dusk mcp:servesurfaces all 31 dusk_* MCP tools even without the fastcli scaffold. Previously returned 0 plugin tools (only the 10 substrate tools). Verified end-to-end on a freshflutter createconsumer with path-linked dusk + artisan 0.0.6 against a running Flutter app on Chrome (real counter increments visible viadusk:tap+ subsequentdusk:snap).
0.0.4 - 2026-05-27 #
Added #
README.md## AI Coding Assistantssection +llms.txt## AI & Toolingsection +📡 AI-first Distributionfeature-table row. Aligns dusk's surface with the cross-package fluttersdk pattern (already shipped onfluttersdk_wind): the canonicalfluttersdk-duskskill atskills/fluttersdk-dusk/is distributed through fluttersdk/ai to 8 agents (Claude Code, Cursor, OpenCode, Gemini CLI, VS Code Copilot, Codex CLI, Cline, Roo Code) vianpx skills add fluttersdk/ai --skill fluttersdk-dusk. The hosted docs MCP atmcp.fluttersdk.comexposes asearch-docstool over Streamable HTTP for direct docs-corpus queries, with annpx @fluttersdk/mcpstdio bridge for clients without HTTP MCP transport. The README copy is explicit that this is independent of dusk's own runtime MCP (./bin/fsa mcp:serve): the docs MCP teaches the agent ABOUT dusk; the runtime MCP gives the agent eyes and hands on a running Flutter app.
Changed #
- Hero logo (
.github/dusk-logo.svg) realigned tofluttersdk_magic1:1. The previous logo had drifted toward indigo (#3730A3,#4338CA,#6366F1,#818CF8) which is not in the magic palette the sibling packages share, and its custom wavy shimmer accents diverged from the family line work. The new SVG is a verbatim copy ofmagic-logo.svg: same 4-layer 3D chevron geometry, same three tilted orbit rings (rotated -12°, 25°, 60° around the same center), samerx/ry/stroke-width/stop-opacitytokens, same 7-color violet palette (#4C1D95through#DDD6FE). The only change is the gradient ID prefix (m*->d*, plusorbit-N->d-orbit-N) so both logos can render on the same page without DOM-level ID collisions. Verification:diff <(grep colors dusk) <(grep colors magic)is empty (set-equal); the same diff overrotate()transforms, ellipse params, chevron paths, and stroke / opacity tokens is also empty.
Fixed #
- README + CI workflow stale
developreferences.README.mdhero logo URL, CI badge?branch=, and contributor-section CI sentence pointed at the retireddevelopbranch (404 after the GitHub Flow migration in 0.0.3). All three now point atmaster..github/workflows/ci.ymlpush + pull_request triggers reduced from[main, master, develop]to[master](single long-lived branch per the new flow;mainwas never used,developis retired). Pub.dev's frozen 0.0.3 archive still carries the broken logo URL; this 0.0.4 docs-only release ships the fix to pub.dev.
0.0.3 - 2026-05-26 #
Added #
skills/fluttersdk-dusk/Section 7 +references/community.md. Opt-in star and issue-report CTAs for the LLM-agent skill, bumped to skillversion: 0.0.3. Section 7 carries the trigger matrix only (star = task verified end-to-end; issue = dusk-side bug, explicitly excluding all six Core Law 3 actionability substrings since those are app-state signals). Executable detail (preflightcommand -v gh && gh auth status,gh api --method PUT /user/starred/fluttersdk/dusk --silent,gh issue create -R fluttersdk/dusk --body-file -heredoc,dusk:doctor+dusk_console+dusk_exceptionsdiagnostic gather, prefill URL fallback under 6KB, spam brakes) lives inreferences/community.mdso the always-loaded SKILL.md body stays compact. Both flows are prose-permission only, maximum once per session, never auto-executed; onghabsence the agent prints the URL but does not invokeopen/xdg-open/start.
Changed #
-
Skill bundle decontaminated from consumer-specific identifiers.
dusk_evaluateexamples inreferences/mcp-tools.md,references/cli-commands.md, andreferences/workflows.mdnow use generic placeholders (MyService.instance.state,MyService.instance.state.toString()) instead of consumer-private symbols (Magic.find<MonitorController>(),Magic.find<MagicApplication>()). Route-discovery hint switched fromgrep -r 'MagicRoute.page'to portablegrep -rEn 'GoRoute|MaterialPage|name:' lib/. -
Tinker REPL guidance unified on the concrete command
./bin/fsa tinkeracross the published skill bundle. Package-name attribution (magic_tinker,artisan_tinker) dropped fromSKILL.md,references/mcp-tools.md,references/workflows.md,references/cli-commands.mdsince users only ever need the command they run. Code-sidemagic_tinkerreferences inlib/src/dusk_artisan_provider.dart,lib/src/extensions/ext_evaluate.dart,ARCHITECTURE.md, anddoc/mcp/tool-reference.mdare unchanged and tracked for a separate follow-up. -
Three Copilot review findings on closed PR #5.
references/mcp-tools.mdIIFE closure now returnsstate.toString()so the placeholder API stays consistent with the surroundingMyService.instance.stateexamples.references/workflows.mdroute-discovery grep uses portablegrep -rEnextended-regex syntax instead of the BSD-incompatible basic-regex\|alternation.skills/fluttersdk-dusk/SKILL.mdstale REPL attribution rewritten. -
Two Copilot review findings on PR #6.
skills/fluttersdk-dusk/SKILL.mdCLI output description rewritten to matchreferences/cli-commands.mdtruth: 9 read / query verbs emit JSON, the 18 side-effect verbs print a one-line success summary by default and only emit JSON when--includeSnapshotis passed.references/community.mdstar-flow note drops the spurious HTTP 304 reference; GitHub'sPUT /user/starred/{owner}/{repo}is idempotent and returns 204 whether the star was new or already set.
Docs #
- CLAUDE.md adopts GitHub Flow (Golden Rule 5 + Branching section). One long-lived branch (
master); task branches cut frommaster, PR back intomaster; releases bumppubspec.yaml+ promote[Unreleased]then tag (git tag X.Y.Z && git push origin X.Y.Ztriggerspublish.yml). Matches flutter/flutter, dart-lang/sdk, dart-lang/pub, and the modern OSS ecosystem (react, vscode, rust, node, kubernetes, go, angular). The repo'sdevelopbranch is retired after this release PR merges.
0.0.2 - 2026-05-24 #
Added #
skills/fluttersdk-dusk/LLM-agent skill bundle. Ships an Anthropic-shape skill that teaches an LLM agent (Claude Code or any MCP client) how to drive a Flutter app wherefluttersdk_duskis installed. Mirrors thefluttersdk_telescopeskill layout. Five files:SKILL.md(frontmatter + 6 core laws + 3 agent loops + tool families + install snippet),references/mcp-tools.md(per-tool input schema / return shape / when-to-use / pitfalls across all 31dusk_*tools),references/cli-commands.md(CLI mirror via./bin/fsa dusk:*, pipeline patterns, exit codes),references/actionability-and-refs.md(6-step gate detail +e<N>/q<N>ref recovery matrix),references/workflows.md(8 concrete agent playbooks: form fill, scroll-to-tap, modal flow, navigation verify, hot-reload-after-edit, pull-to-refresh, log tail, before/after diff). Frontmatter front-loadsTRIGGER when:/DO NOT TRIGGER when:vocabulary so the model auto-loads the skill on anydusk_*MCP call,dusk:*CLI invocation, or E2E-driver task on a running Flutter app.
Docs #
- README demo.gif placeholder removed. The
<p align="center"><img src=".../screenshots/demo.gif"></p>block plus itsTODO(v0.0.2-followup)recording-instructions comment are dropped until the actual asset ships. The hero logo, badges, and below-the-fold content are unchanged. - example showroom em-dash sweep.
example/lib/main.dartsection headers and inline comments are normalised to commas / colons / parentheses, aligning the example with the global no-em-dash rule applied across the rest of the repo.
0.0.1 - 2026-05-23 #
Initial public release of fluttersdk_dusk. E2E driver for Flutter apps. Snapshot, tap, type, drag, scroll, screenshot, wait, find via VM Service extensions (ext.dusk.*). Framework-agnostic (vanilla Flutter friendly); Magic / Wind integrations ship inside those packages via DuskPlugin.enrichers extension point. Plugin of fluttersdk_artisan ^0.0.5 (hosted-only; no path overrides). Wind diagnostics flow through the neutral fluttersdk_wind_diagnostics_contracts bridge (WindDebugRegistry) rather than through the enricher list, so wind alpha-10 needs no dusk-side install wiring.
Added #
- 32 CLI commands via
DuskArtisanProvider.commands()(live count fromls lib/src/commands/*_command.dart):dusk:install,dusk:snap,dusk:tap,dusk:screenshot,dusk:type,dusk:scroll,dusk:wait,dusk:wait_for_network_idle,dusk:hover,dusk:drag,dusk:modal,dusk:doctor,dusk:navigate,dusk:navigate_back,dusk:get_routes,dusk:press_key,dusk:select_option,dusk:close_app,dusk:find,dusk:focus,dusk:blur,dusk:clear,dusk:right_click,dusk:dblclick,dusk:triple_click,dusk:set_checkbox,dusk:console,dusk:exceptions,dusk:observe,dusk:resize,dusk:device,dusk:hot_reload_and_snap.dusk:installis the one-shot bootstrap; the rest wrap a matching VM Service extension or substrate-routed action. - 31 MCP tool descriptors via
DuskArtisanProvider.mcpTools()(live count fromgrep "name: 'dusk_" lib/src/dusk_artisan_provider.dart | sort -u):dusk_blur,dusk_clear,dusk_close_app,dusk_console,dusk_dblclick,dusk_device_profile,dusk_dismiss_modals,dusk_drag,dusk_evaluate,dusk_exceptions,dusk_find,dusk_focus,dusk_get_routes,dusk_hot_reload_and_snap,dusk_hover,dusk_navigate,dusk_navigate_back,dusk_observe,dusk_press_key,dusk_resize_viewport,dusk_right_click,dusk_screenshot,dusk_scroll,dusk_select_option,dusk_set_checkbox,dusk_snap,dusk_tap,dusk_triple_click,dusk_type,dusk_wait_for,dusk_wait_for_network_idle. AllMcpToolDescriptorconst instances with Claude Code canonical descriptions (imperative opener + context paragraph +Usage:bullets). - 28 ext.dusk. VM Service extensions + 3 artisan:dusk: substrate-routed tools** (live count from
grep "extensionMethod:" lib/src/dusk_artisan_provider.dart | sort -u). Direct ext.dusk.:snap,screenshot,tap,hover,drag,type,scroll,wait_for,wait_for_network_idle,dismiss_modals,press_key,select_option,navigate,navigate_back,get_routes,evaluate,close_app,find,focus,blur,clear,right_click,dblclick,triple_click,set_checkbox,console,exceptions,observe. Substrate-routed viaartisan:dusk:*:resize,device,hot_reload_and_snap(in-isolate hot-reload deadlock avoidance). All ext.dusk. extensions register throughregisterExtensionIdempotentfor hot-restart safety. DuskPlugin.install(); idempotent host-side install entry. Wraps the app widget root in aRepaintBoundary(noGlobalKey) soext.dusk.screenshotcan find it via render-tree walk. Hot-restart safe via static_installCountguard. HonorsDUSK_DISABLEenv var (1/true/yes, case-insensitive) as kill switch.DuskSnapshotEnrichertypedef; snapshot-enricher extension point.String? Function(Element, RefRegistry). Magic ships its enrichers viaMagicDuskIntegration. Wind no longer ships an enricher as of wind alpha-10: wind state is read through the neutralfluttersdk_wind_diagnostics_contracts.WindDebugRegistry.current?.resolve(element)bridge insideext_snapshot.dartandext_observe.dartahead of the enricher loop, so the 6 core wind fields (breakpoint, brightness, platform, states, bgColor, textColor) survive without an enricher registration. Contract: synchronous, stateless w.r.t. call ordering, may returnnullto skip, multi-line fragments split + indented under the ref entry by the dispatcher.fluttersdk_wind_diagnostics_contractsintegration: new production depfluttersdk_wind_diagnostics_contracts: ^1.0.0.ext.dusk.snapandext.dusk.observeread wind state viaWindDebugRegistry.current?.resolve(element)in addition to the existing enricher list dispatch; thewind:block (filtered by_kDefaultWindKeysindefaultsmode) is emitted directly by dusk. Magic enricher contract UNCHANGED.RefRegistry; stablee<N>(snapshot-frozen) andq<N>(re-resolvable Playwright-Locator) token systems.e<N>refs are minted atdusk_snaptime and consumed by every action tool;q<N>refs are minted bydusk:findand re-execute their stored predicates against the live tree on every action call (resilient to widget rebuild + route push).- Actionability gate (
lib/src/utils/actionability_gate.dart);tap/hover/drag/typeresolve through a single gate that verifies the target's enabled flag (Tristate.isFalsefails;Tristate.noneandTristate.isTruepass), zero-area rect, and viewport overlap BEFORE synthesising the pointer / key event. Failures surfaceServiceExtensionResponse.error(extensionError, "Widget ref=$ref is not actionable: $reason")with$reason∈ {"not enabled","zero rect","off-viewport (rect=..., viewport=...)"}.scroll,select_option, andpress_keyintentionally skip the gate (see Known gaps). dusk:installone-shot bootstrap; minimal install. Edits the consumer'slib/main.dartonly (nobin/artisan.dartorlib/app/scaffolding for vanilla Flutter apps). Detects Magic-stack apps via theawait Magic.init(anchor and injectsDuskPlugin.install()BEFORE Magic.init (thenMagicDuskIntegration.install()AFTER), falling back to therunApp(anchor for vanilla Flutter apps. Wind alpha-10 needs no install-time wiring from dusk: the consumer callsWind.installDebugResolver()directly, and dusk reads wind state throughWindDebugRegistryat snap time. Vanilla consumers access dusk viadart run fluttersdk_dusk <cmd>. Idempotent; safe to re-run.- Flutter-free CLI wrapper;
bin/fluttersdk_dusk.dart+executables: fluttersdk_duskpubspec entry.dart run fluttersdk_dusk <cmd>proxies the full artisan CLI surface and exposes the dusk commands without draggingdart:uiinto pure-Dart contexts. install.yamlplugin manifest; V1 manifest at the package root makesplugin:install fluttersdk_duskwork end-to-end via the artisanPluginInstaller.lib/cli.dartcodegen barrel; Flutter-free typedef aliasFluttersdkDuskArtisanProvider. Consumed by consumer-sidelib/app/_plugins.g.dartauto-discovery without pulling Flutter symbols into the pure-Dart artisan codegen path.dusk:findPlaywright-Locator pattern; mintsq<N>query handles backed bytext/semanticsLabel/keypredicates. Unlikee<N>refs (frozen at snap time), q-handles re-execute the Semantics + Element walk on every action call, so they survive widget rebuilds and route pushes as long as the predicates still match. Stale match returns an explicitstale-handleerror; the agent re-finds, never silently retries.dusk:doctor; diagnostic command that checks~/.artisan/state.jsonChrome PID staleness,DUSK_DISABLEenv-var value, registered enricher count, Semantics-tree-forced flag, and Magic-init wiring in one pass. Emits a categorised report (OK / WARN / ERROR per check); exit code 0 when every check passes.- Chrome reaper (
lib/src/utils/chrome_reaper.dart); graceful Chromium subprocess teardown between dusk:* runs so leftover headless tabs no longer accumulate. Detects orphans by VM Service URI, exits cleanly viaSystemNavigator.popfirst, falls back to SIGTERM. - Example apps:
example/(vanilla Flutter, 7 scenario screens: home menu + buttons / inputs / scroll / modals / drawer / forms) for live e2e validation against the 31 MCP tools + 32 CLI commands. - CDP driver (
lib/src/cdp/):CdpClient,DevicePresets(8 curated device presets with explicit DPR values:iphone-x,iphone-13,iphone-15-pro,pixel-5,pixel-8,ipad-pro-12.9,desktop-1440,desktop-1920),ChromeFinder. Minimal in-house Chrome DevTools Protocol client (~110 LoC, dart:io WebSocket + dart:convert; no pub.dev deps). dusk:resizeCLI (lib/src/commands/dusk_resize_command.dart):dart run fluttersdk_dusk dusk:resize --width=375 --height=812 [--dpr=3] [--mobile] [--touch]. ReadscdpPortfrom state.json, opensCdpClient, sendsEmulation.setDeviceMetricsOverride(+ optionalsetTouchEmulationEnabled).--resetsends 3-call clear chain. Fails loudly when CDP not enabled.dusk:deviceCLI (lib/src/commands/dusk_device_command.dart):dart run fluttersdk_dusk dusk:device --preset=iphone-x. Applies the full emulation chain (metrics + conditional touch + UA) from the curated preset database.--listprints all 8 preset entries;--resetmirrorsdusk:resize --reset.- 2 CDP MCP tools (
dusk_resize_viewport+dusk_device_profile): both dispatch via the existingartisan:substrate prefix (nomcp_server.dartchanges). FakeCdpServertest harness (test/src/cdp/fake_cdp_server.dart): dart:ioHttpServer+WebSocketTransformer.upgradeon an ephemeral loopback port. Configurable failure modes (failOnJsonVersion,dropWebSocket,delayResponseMs). Used bycdp_client_test.dart,dusk_resize_command_test.dart,dusk_device_command_test.dart.- Integration smoke test (
test/integration/cdp_smoke_test.dart): tagged@Skipso defaultflutter testskips it; run manually viaflutter test test/integration --tags integrationto validatedart-lang/webdev#2642regression status. dusk:installmagic-detect branch: now injectsimport 'package:magic/dusk_integration.dart';instead ofimport 'package:magic/magic.dart';. Pairs with magic 1.0.0-alpha.15 which extracts the integration class into a dedicated sub-barrel.- 6-step actionability gate (Wave 3): Step 0 defunct preflight + Stable + Receives-Events gates round out
ensureActionable(now async). Total preconditions in evaluation order: defunct (preflight), enabled, zero-rect, off-viewport, stable (rect unchanged across 2 consecutive frames; Playwright auto-waiting), receives-events (hit-test confirms ref is the front-most pointer target). Opt-out viacheckStable=false/checkReceivesEvents=false(both defaulttrue). Failure-reason substrings extended:"defunct","not stable","obscured by"join the existing agent branch surface. - Snapshot-in-action-response (Wave 3, Playwright
setIncludeSnapshotpattern): 8 action handlers (tap,hover,drag,type,press_key,scroll,navigate,navigate_back) acceptincludeSnapshot=trueand append the post-action snapshot YAML to the success response. The agent no longer needs a mandatory follow-updusk_snapcall.duskSnapBuildwidened from@visibleForTestingto public (legitimate production reuse).press_keyhandler endOfFrame omission fixed in passing. - Structured error envelope + fuzzy-match suggestions (Wave 3):
lib/src/utils/error_envelope.dartwithDuskErrorEnvelopecarryingtype+widget_path+suggestions[]. 10 type values:timeout,not_found,obscured,disabled,stale,zero_rect,off_viewport,not_stable,missing_param,unexpected. 6 factories. Dual-write intoerrorDetail(JSON envelope alongside the free-form message) preserves backward compat for substring-matching agents. Levenshtein with prefix-bonus drives the suggestions list fornot_found.RefRegistry.activeRefs()added to support candidate collection. ext.dusk.wait_for_network_idle(Wave 3): pollsTelescopeStore.pendingHttpCountuntil the count hits zero for a configurableidleMswindow. ParamstimeoutMs(5000),idleMs(500),pollIntervalMs(200). Function-pointer indirection (pendingHttpCountReaderexported fromdusk.dart) keeps dusk free of a hard telescope dependency; magic-side wires the real reader at install time. New CLI commanddusk:wait_for_network_idle.- 4 utility tools (Wave 3):
dusk_console(telescope log reader, function-pointer indirection viarecentLogsReader),dusk_exceptions(telescope exception reader viarecentExceptionsReader),dusk_dblclick(two synthesised taps with 100ms inter-tap delay, shared 6-step actionability gate + snapshot embed),dusk_set_checkbox(idempotentCheckbox/Switchtoggle via element walk; no-op when current value matches target). ext.dusk.observe(Wave 4): Stagehand-style observe-once-act-many pattern. Walks every activePipelineOwnersemantics tree, filters interactive nodes (buttons / textfields / links / checkboxes / dropdowns via_roleFor/_isInteractive), mints a re-resolvableq<N>ref per candidate (Playwright Locator pattern; nevere<N>), and returns a structured JSON list{candidates: [...], count: N}. Each candidate carriesref,role,label,value,bounds,isEnabled,isVisible, plus enricher-projected fields. Params:intent(caller hint, echoed only),limit(default 50),roles(comma-separated filter),includeEnrichers.dusk:hot_reload_and_snap(Wave 4): CLI-side orchestration viaVmServiceClient.reloadSources(in-isolate handler cannot reload its own isolate; deadlock avoidance). Sequence: reload -> wait -> snap -> screenshot -> exceptions -> bundle. Success envelope{reloaded, durationMs, snapshot, screenshot, recentExceptions}; compile-error envelope skips snap/screenshot but still gathers exceptions. Screenshot failure surfaces as partial-resultscreenshotErrorrather than aborting the round-trip. MCP descriptor uses theartisan:substrate routing prefix (extensionMethod: 'artisan:dusk:hot_reload_and_snap').dusk:installis now self-sufficient (Wave 5 pre-publish). Phase 1 patcheslib/main.dart(unchanged contract). Phase 2 chainsdart run fluttersdk_dusk install(scaffoldsbin/dispatcher.dart+./bin/fsaAOT wrapper) followed bydart run fluttersdk_dusk plugin:install fluttersdk_dusk(registersDuskArtisanProvider; artisan 0.0.5 auto-purges the AOT bundle cache). Both Phase 2 sub-process calls are file-marker-guarded (bin/dispatcher.dart,.artisan/installed/fluttersdk_dusk.json) so re-runs are fast no-ops; failures swallow with a warning so Phase 1'slib/main.dartinject remains the guaranteed contract regardless of the consumer'sdartPATH / sandbox state. Net effect: a fresh consumer needs onlyflutter pub add fluttersdk_dusk+dart run fluttersdk_dusk dusk:installto reach a working./bin/fsa list+ MCPtools/listsurface.ext.dusk.findsubstring predicate +dusk:find --contains=<substring>CLI flag (Wave 5; pre-publish E2E pass). Existing--text=<exact>semantics unchanged; agents now have a brittle / dynamic-label fallback.DuskQuery.containsTextfield is the carrier; matching walks Semantics labels first, thenText.data, mirroring thetextpath.dusk:drag --fromRef=<eN> --toRef=<eN>flag aliases parallel to the--refshape used bydusk:tap/dusk:hover(Wave 5). Legacy--startRef/--endRefflags retained for back-compat.dusk:scroll --direction=<up|down|left|right> --pixels=<N>convenience flags that translate to signed--dy/--dx(Wave 5). Explicit--dy/--dxstill win when both forms supplied.- Surface deltas (live counts): CLI commands: 32 (
lib/src/commands/*_command.dart); MCP tool descriptors: 31 (dusk_artisan_provider.dart); VM Service extensions: 28ext.dusk.*+ 3artisan:dusk:*substrate-routed.
Fixed (pre-publish macOS + web E2E pass, Wave 5) #
dusk_resize_viewportMCP arg parsing (GAP I): handler castctx.input.option('width') as String?which failed when MCPtools/calldelivers{"width":390}as a native JSON int rather than a stringified arg. Resize command now defensively readsint/double/boolfrom either type via_readInt/_readDouble/_readBoolhelpers. CLI invocations still work unchanged (ArgParser-emitted strings).
Fixed #
ext.dusk.focuson TextField + EditableText (GAP C): handler walked UP from the snap-captured Semantics element looking for aFocusancestor; for TextField the FocusNode sits BELOW the captured element (insideEditableText/FocusableActionDetector). Now falls back to a descendant walk that picks the firstEditableText.focusNodeorFocus.focusNodeit finds. Reproducer:dusk:focus --ref=<textbox-eN>previously returnedno Focus ancestor; now returnsfocused: true.ext.dusk.scrollwith ref pointing at the Scrollable itself (GAP D):Scrollable.maybeOf(context)walks UP, so passing the ListView's own ref (e.g. fromdusk:find --key=my-list) returned null. Handler now resolves in three stages: (1) target element IS a Scrollable, use its state; (2) Scrollable ancestor (legacy); (3) descendant Scrollable walk (when ref is a parent like a Scaffold wrapping a list).dusk:press_key --key=case-sensitivity (NIT 5): agents calling--key=TABor--key=enterhitunknown keyeven though the supported set covered the intent. Lookup now does a case-insensitive fallback over_kKeyMap.keyswhen the direct hit misses; canonical PascalCase keys (Tab,Enter,ArrowUp) remain documented.dusk:screenshotsuccess message now reports decoded byte count + KB + format, e.g.Wrote 239456 bytes (233.8 KB, jpeg) to ./shot.jpg(NIT 1). Previously the line referenced the base64 character count which misled agents parsing for byte size.dusk:screenshotmissing-output error now suggests the canonical invocationdusk:screenshot --output=./shot.jpg --format=jpeg(NIT 8).- README + installation.md document the full 3-step install flow:
flutter pub add fluttersdk_dusk+dart run fluttersdk_dusk dusk:install+dart run fluttersdk_dusk install && dart run fluttersdk_dusk plugin:install fluttersdk_dusk(GAP B). Previously theplugin:installstep was missing, leaving consumers with./bin/fsa listshowing 0 dusk:* commands.installation.mdcarries a new## Register with artisansection explaining the fastcli scaffold + plugin registration.
Test coverage #
- 678 tests passing (2026-05-23 pre-publish,
flutter test --exclude-tags=integration --timeout=30s). Scope covers handler entry points (params + error paths + happy paths where reachable underflutter_test), 32 CLI commands (name / boot / description / configure / handle / missing-arg validation),DuskArtisanProvider.commands()/mcpTools()shape,DuskPlugin.install()idempotency +DUSK_DISABLEenv-var kill switch,RefRegistrymint / lookup / disposeGroup / disposeAll / refsForGroup / registerQuery / lookupQuery, actionability gate (6-step: defunct / enabled / zero-rect / off-viewport / not-stable / obscured),encodeToJpegPNG-to-JPEG roundtrip + quality boundaries (1, 100, error), modal-route classification, dispatcher contract, CDP client + device presets + resize/device commands, Wave 3 structured error envelopes, Wave 4 observe + hot-reload-and-snap, Wave 5 find-contains substring + descendant focus walk + Scrollable-own-ref scroll. Pre-publish E2E pass against a fresh vanilla Flutter consumer (/tmp/dusk_e2e) verified 27 of 32 CLI commands + MCPinitialize+tools/list(41 tools = 31 dusk_* + 10 artisan_*) +tools/call dusk_snap(identical to CLI) +tools/call dusk_evaluate(actual evaluation via artisan 0.0.5 substrate routing). - Coverage: dusk ~79% line coverage via
flutter test --coverage. The remaining gap covers engine-dependent paths that hang theflutter_testfake-clock harness: handlerendOfFramewaits,Future.delayedpoll loops inwait_for, realtoImage()rasterisation inscreenshotsuccess paths, and private_defaultProcessStartTime/_parsePsLstartdoctor seam defaults. End-to-end coverage for those paths is captured by the example/ playground sweep.
Known gaps #
dusk:doctorruns in pure-Dart CLI context and cannot importpackage:flutter/rendering.dartwithout draggingdart:ui(breaksdart runinvocation). Two checks defang gracefully as a result:semanticsEnabledProbedefaults totrue(the only ERROR-class check, so doctor cannot ERROR from CLI) andenrichersProbedefaults to0(always WARNs on Check 3). The real probes belong to a future VM-Service-attached doctor invocation that calls into the running app.scroll,select_option, andpress_keyintentionally skip the actionability gate: scroll targets the parent scrollable not the ref, select_option dispatches through Material/Cupertino popup machinery that owns its own enabled check, and press_key targets the focused widget rather than a ref. Adding the gate to these three handlers is V1.x candidate work.RefRegistry._queries(q-handle store) is monotonically growing within a debug session; onlyRefRegistry.disposeAll()clears it. Worst-case memory bounded by debug-session lifetime; per-handle eviction is V1.x candidate work.
Risks Accepted #
dart-lang/webdev#2642live regression: "Hot restart broken when running DWDS without Chrome Debug Port". Integration smoke test (test/integration/cdp_smoke_test.dart) surfaces this if active. Mitigation lives in the user's pinned Flutter SDK; plan does not block on regression resolution.- Flutter SDK >= 3.30.0 required for
--cdp-port(perflutter/flutter#170612). Lower versions get an actionable error from bothartisan doctor(advisory) andartisan start --cdp-port(fail-fast). - GAP E (drag synthesis vs Flutter Draggable):
dusk:dragreturns success but Flutter'sDragTarget.onAcceptWithDetailsdoes not fire on synthesised events in some configurations (Pointer Down + 5x Move + Up sequence may not match Draggable's gesture recognizer expectations on certain platforms / dwell times). Verified via E2E showroom (2026-05-23). Tracked for a 0.0.2 follow-up; agents needing drag should fall back to a pair ofdusk:tap+ manual scroll for now. - GAP G (advisory): receives-events check + q-refs on widgets with deep render subtrees: when
dusk:find --key=<name-field>resolves to aTextField(or any widget whosefindRenderObject()returns a top-level RenderObject), the actionability gate's receives-events check sees a hit-test path topped by a deeper descendant (e.g.RenderEditable) and tripsobscured by other widget. The_isDescendantOfwalk does not catch this case consistently. Workarounds: (1) use thee<N>ref from a priordusk:snaprather than aq<N>from--key; (2) pass--no-checkReceivesEventson the action. Tracked for a 0.0.2 follow-up; deeper investigation needed in the gate's hit-test path traversal. - GAP H (web):
dusk:screenshot+dusk:close_apptimeout on Chrome (DWDS): 10s timeout. macOS desktop works fine. The web path likely needs special handling forRepaintBoundary.toImage()under DWDS pixel pipeline + the platform-close semantics ofSystemNavigator.pop()(which closes the tab, so the response can't return). Workaround for close: rely on./bin/fsa stopSIGTERM (works). Workaround for screenshot on web: use the browser DevTools snapshot. Tracked for a 0.0.2 follow-up.
Backward compat #
DuskSnapshotEnricher typedef, DuskPlugin.install / DuskPlugin.enrichers / DuskPlugin.registerNavigateAdapter, RefRegistry public methods (register, lookup, registerQuery, lookupQuery, disposeAll, resetForTesting), and every MCP tool name / ext.dusk.* extension name are part of the public 0.0.1 contract. Future releases keep these stable across the 0.x line; any change requires a coordinated bump with magic + wind.