# Dusk Documentation (Full) > Consolidated documentation for context window loading. --- # Getting Started # Getting Started Everything you need to add `fluttersdk_dusk` to your project, capture your first snapshot, and drive real user gestures from an AI agent or CLI session. ## Pick your path - [**Installation**](installation): Wire DuskPlugin into a Flutter app and connect the MCP tools to your AI client. - [**Quickstart**](quickstart): 3-step walkthrough from zero to driving the example app with snap, tap, and screenshot. - [**MCP setup**](../mcp/setup): Register the dusk MCP tools in Claude Code, Cursor, Windsurf, or any MCP-compliant client. ## What is fluttersdk_dusk? `fluttersdk_dusk` is a Flutter E2E driver that gives an AI agent (or a CLI session) eyes and hands over a running Flutter app. It works by registering a set of VM Service extensions under the `ext.dusk.*` namespace during debug builds, then exposing those extensions as 31 MCP tools and 32 CLI commands. The agent snaps a Semantics YAML to identify widget references, then drives gestures, text input, scrolling, and navigation against those references without any test instrumentation in the production widget tree. What sets it apart from `flutter_test`-based integration tests is that it operates against the live running app, not a test-hosted widget. The agent sees the same UI the user sees: real platform chrome, real routing, real auth state. This makes it suitable for exploratory workflows where the agent does not know the widget tree in advance and needs to discover it interactively. ## What problem does it solve? Writing Flutter integration tests requires knowing the widget tree structure upfront, hard-coding `find.byKey` / `find.byType` locators, and re-running the full suite every time the UI changes. That works well for regression coverage but poorly for exploratory agent-driven automation, where the agent needs to navigate an unfamiliar UI, read its current state, and take adaptive actions. `fluttersdk_dusk` solves this by exporting the Semantics tree as a YAML snapshot with stable `e` (snapshot-frozen) and re-resolvable `q` (find-minted) ref tokens. The agent reads the snapshot, locates the target element by semantic role or label, and passes the ref token to an action extension. No test harness setup, no build step, no `flutter drive` orchestration needed. ## When to use it - **AI agent walkthroughs**: give Claude Code, Cursor, or Windsurf hands-on control of your Flutter app so it can inspect, fill forms, and verify flows during development or review. - **Manual E2E scripting**: drive gestures and input from a terminal session without writing widget tests, useful for ad-hoc QA against a staging build. - **Screenshot pipelines**: capture consistent screenshots of specific app states for documentation, design review, or visual regression baselines. - **CI smoke checks**: run a small set of artisan commands in CI to confirm critical paths render without crash, complementing (not replacing) widget unit tests. ## Requirements | Dependency | Minimum Version | Notes | |:-----------|:----------------|:------| | Dart | `>= 3.4.0` | Records, sealed classes, class modifiers. | | Flutter | `>= 3.22.0` | `RepaintBoundary` render-tree walk and Semantics APIs used internally. | | fluttersdk_artisan | `^0.0.3` | Provides the MCP server, CLI framework, and `registerExtensionIdempotent`. | `fluttersdk_dusk` requires Flutter. It cannot run on a pure-Dart environment because it synthesizes pointer events against a live widget tree and walks the Semantics tree at runtime. The `fluttersdk_artisan` CLI is the recommended host for the MCP server and all dusk CLI commands; install it before adding `fluttersdk_dusk`. ## High-level workflow The typical agent session follows three phases: ``` 1. Capture snapshot -- dusk_snap / dusk:snap Walks the Semantics tree and emits a YAML file with every visible widget, its ref token, role, label, bounds, and any enricher-contributed metadata (className, MagicRoute, ...). 2. Identify a ref -- read the YAML / dusk_find The agent scans the snapshot for the target widget by label or role, reads its ref token (e1, q3, ...). dusk_find re-resolves a token without a full re-snap. 3. Drive an action -- dusk_tap / dusk_type / dusk_scroll / ... The agent passes the ref token to an action tool. The extension looks up the live element, checks the actionability gate (enabled, non-zero-rect, on-viewport), synthesizes the event, and returns the result or a structured error. ``` After an action the agent re-snaps to observe the updated UI and continues the loop. Screenshot requests can be inserted at any point to capture a PNG of the current viewport. ## Next steps - New here? Start with [Installation](installation). - Already installed? Run the [Quickstart](quickstart). --- # Installation # Installation - [Requirements](#requirements) - [Add the package](#add-the-package) - [Wire DuskPlugin](#wire-duskplugin) - [Optional integrations](#optional-integrations) - [Wire MCP tools](#wire-mcp-tools) - [Verify installation](#verify-installation) Getting `fluttersdk_dusk` running requires adding the package, calling `DuskPlugin.install()` inside a `kDebugMode` guard in your app's `main.dart`, and (optionally) wiring the MCP server so your AI client can reach the dusk tools. ## Requirements `fluttersdk_dusk` is a Flutter package. It requires a Flutter app target for the VM Service extensions to register against. Pure-Dart environments are not supported. | Dependency | Minimum | Recommended | |:-----------|:--------|:------------| | Dart | `>= 3.4.0` | `3.6.0+` | | Flutter | `>= 3.22.0` | `3.27.0+` | | fluttersdk_artisan | `^0.0.3` | latest | Install `fluttersdk_artisan` first if it is not already in your project. It provides the MCP server, the CLI framework, and the `registerExtensionIdempotent` helper that dusk uses internally for hot-restart safety. ```bash dart pub add fluttersdk_artisan ``` ## Add the package Add `fluttersdk_dusk` using the Flutter CLI: ```bash flutter pub add fluttersdk_dusk ``` Alternatively, add it manually to `pubspec.yaml`: ```yaml dependencies: fluttersdk_dusk: ^0.0.2 ``` Then fetch dependencies: ```bash flutter pub get ``` ## Wire DuskPlugin The recommended path is the CLI installer. From your project root, run: ```bash dart run fluttersdk_dusk dusk:install ``` This patches your `lib/main.dart` automatically: it adds the `kDebugMode` import, wraps `DuskPlugin.install()` in a `kDebugMode` guard, injects `WidgetsFlutterBinding.ensureInitialized()` when missing, and detects Magic-stack apps so `MagicDuskIntegration.install()` lands AFTER `Magic.init(...)`. The command is idempotent; re-running it is safe. See [`dusk:install`](../commands/dusk-install.md) for the full sub-step list and the anchor strings the injector searches for. ### Manual wiring (when you'd rather edit `main.dart` yourself) Skip the CLI installer and edit `lib/main.dart` directly. Call `DuskPlugin.install()` inside a `kDebugMode` guard, after `WidgetsFlutterBinding.ensureInitialized()` and before `runApp()`. The guard is mandatory: release builds tree-shake the entire subsystem, so dusk never ships to end users. ```dart import 'package:flutter/foundation.dart'; import 'package:fluttersdk_dusk/dusk.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); if (kDebugMode) { DuskPlugin.install(); } runApp(const MyApp()); } ``` `DuskPlugin.install()` is idempotent: calling it more than once (for example during hot restart) is safe and registers each VM Service extension only once. ## Optional integrations Call additional `install()` methods inside the same `kDebugMode` block to enrich snapshots with framework-specific metadata. Both integrations are independent; install either, both, or neither depending on your stack. | Integration | Package | Enrichment | |:------------|:--------|:-----------| | `MagicDuskIntegration.install()` | `magic` | MagicForm field values, validation state, named route per node. | | `Wind.installDebugResolver()` (in `package:fluttersdk_wind/fluttersdk_wind.dart`) | `fluttersdk_wind` >= alpha-10 | Wind state surfaces through the neutral `WindDebugRegistry` bridge; dusk emits the 6 core fields (breakpoint, brightness, platform, states, bgColor, textColor) automatically without enricher registration. | ```dart if (kDebugMode) { DuskPlugin.install(); MagicDuskIntegration.install(); // magic-stack only Wind.installDebugResolver(); // wind UI only (alpha-10+) } ``` See [Magic integration](../plugins/magic-integration) and [Wind integration](../plugins/wind-integration) for the full enricher field reference. ## Register with artisan (automatic) `dusk:install` already handles this for you as Phase 2 of its run. Right after the `lib/main.dart` patch lands, it chains `dart run fluttersdk_dusk install` (scaffolds `bin/dispatcher.dart` + `./bin/fsa` fastcli, ~110ms warm AOT) followed by `dart run fluttersdk_dusk plugin:install fluttersdk_dusk` (registers `DuskArtisanProvider` and auto-purges the AOT bundle cache). Both sub-process calls skip when their idempotency markers already exist (`bin/dispatcher.dart` and `.artisan/installed/fluttersdk_dusk.json` respectively), so re-running `dusk:install` is a fast no-op. If the chained calls fail (no `dart` on PATH, partial pub-cache, restricted sandbox), `dusk:install` falls through with a warning and exits 0; the `lib/main.dart` patch already landed, so `dart run fluttersdk_dusk ` still works. You can finish the setup manually: ```bash dart run fluttersdk_dusk install # only if Phase 2 was skipped dart run fluttersdk_dusk plugin:install fluttersdk_dusk # idempotent; refreshes barrels on re-run ``` After Phase 2 lands, `./bin/fsa list` shows all `dusk:*` commands and `./bin/fsa mcp:serve` exposes the 31 dusk_* tools. The optional `mcp:install` step in the next section writes the plugin-aware `.mcp.json` payload by default. When fastcli (`./bin/fsa`) is present, the entry uses `./bin/fsa mcp:serve`; when it is absent, the wrapper's `--invocation=fluttersdk_dusk` pass-through causes `mcp:install` to write `dart run fluttersdk_dusk mcp:serve` instead. Either way, no manual `.mcp.json` edit is needed. ## Wire MCP tools With artisan registered, expose dusk's 31 MCP tools to your AI client by writing the `.mcp.json` entry: ```bash dart run fluttersdk_dusk mcp:install ``` This writes (or updates) a `.mcp.json` file at the project root. Claude Code, Cursor, and Windsurf all pick up `.mcp.json` automatically from the working directory. The payload depends on your scaffold state: - **fastcli present** (`./bin/fsa` exists, POSIX): writes `./bin/fsa mcp:serve` — fastest startup (~50ms warm AOT). - **fastcli absent**: the wrapper injects `--invocation=fluttersdk_dusk` automatically, so the entry writes `dart run fluttersdk_dusk mcp:serve` (~3s startup, no scaffold required). See [Fallback invocations](../mcp/setup.md#fallback-invocations) for the full precedence table. ## Verify installation Start your Flutter app in debug mode on a device or browser: ```bash flutter run -d chrome ``` Then, in a separate terminal, capture the first Semantics snapshot: ```bash dart run fluttersdk_dusk dusk:snap ``` A successful snap prints a YAML block beginning with `snapshot:` followed by one or more indented widget nodes. Each node carries a `ref:` token (`e1`, `e2`, ...) that you pass to action commands. If the command exits with an error, confirm that `DuskPlugin.install()` is reachable in your `main.dart` and that the app is running in debug mode (VM Service extensions do not register in profile or release builds). --- # Quickstart # Quickstart A 3-step walkthrough from zero to driving the bundled `example/` Flutter app with a snap, a tap, and a screenshot via the dusk CLI. Prerequisites: Flutter `>= 3.22.0` SDK installed, `fluttersdk_dusk` added to your project (see [Installation](installation)), and `DuskPlugin.install()` wired inside `kDebugMode` in your `main.dart`. --- ### 1. Start the example app and capture a snapshot Clone the repository (or navigate to the package root if you are working from source), then launch the bundled example app in Chrome: ```bash cd example flutter run -d chrome ``` Leave the browser window open and open a second terminal in the same directory. Run `dusk:snap` to walk the live Semantics tree and emit a YAML snapshot: ```bash dart run fluttersdk_dusk dusk:snap ``` The command connects to the running app via the VM Service, walks every visible Semantics node, and prints a snapshot to stdout. A minimal output looks like this: ```yaml snapshot: - ref: e1 role: button label: "Increment" bounds: {left: 312, top: 548, width: 56, height: 56} - ref: e2 role: text label: "You have pushed the button 0 times." bounds: {left: 100, top: 300, width: 600, height: 24} - ref: e3 role: text label: "0" bounds: {left: 100, top: 340, width: 600, height: 48} ``` Each node has a `ref:` token (here `e1`, `e2`, `e3`). These tokens are stable for the lifetime of the snapshot and are what you pass to action commands in the next step. --- ### 2. Tap a widget using its ref token Locate the ref token for the target widget in the snapshot output above. The counter increment button is `e1`. Pass that token to `dusk:tap`: ```bash dart run fluttersdk_dusk dusk:tap --ref=e1 ``` The extension looks up `e1` in the frozen snapshot registry, checks the actionability gate (the widget must be enabled, have a non-zero bounding rect, and be on-viewport), then synthesizes a pointer-down + pointer-up event pair at the widget's center. The app responds as if a real user tapped the button. A successful tap returns a JSON confirmation: ```json {"action": "tap", "ref": "e1", "label": "Increment", "ok": true} ``` If the widget fails the actionability gate, the command exits with an error message: ``` Widget ref=e1 is not actionable: not enabled ``` Re-snap (`dusk:snap`) after a tap to confirm the UI updated. In this example the counter label `e3` should now show `"1"` in the refreshed snapshot. --- ### 3. Capture a screenshot Capture a PNG of the current viewport to verify the app state visually or to save a baseline image: ```bash dart run fluttersdk_dusk dusk:screenshot --output=counter_after_tap.png ``` The extension walks the render tree to find the `RepaintBoundary` that `DuskPlugin.install()` wraps around the app root, captures a raster frame, and writes the PNG to the path you specify. The file lands relative to your current working directory. Expected output: ``` Screenshot saved to counter_after_tap.png (1280x800) ``` Open the file to confirm the counter reads 1 after the tap in step 2. You now have the full snap-tap-screenshot loop working end-to-end. --- ## What's next? - Read the [commands catalog](../commands/) to see all 32 dusk CLI commands and their flags. - Set up the [MCP server](../mcp/setup) so Claude Code or Cursor can call dusk tools directly during a conversation. - Add [MagicDuskIntegration](../plugins/magic-integration.md) (Magic stack) or call `Wind.installDebugResolver()` (Wind alpha-10+; see [Wind integration](../plugins/wind-integration.md)) to enrich snapshots with framework-specific metadata. - Explore the [actionability gate reference](../reference/actionability-gate) to understand how dusk decides whether a widget is safe to interact with. --- All three commands shown above are part of the `fluttersdk_dusk` built-in surface. No additional packages are needed beyond `fluttersdk_artisan` (MCP server and CLI host) and `fluttersdk_dusk` (VM Service extensions) installed in step 1. --- # Commands # Commands Catalog of every user-facing command shipped by `fluttersdk_dusk`. Thirty-two commands, grouped by intent. Every command is invoked as `dart run fluttersdk_dusk ` (Flutter-free wrapper at `bin/fluttersdk_dusk.dart`), or via the consumer-side artisan dispatcher (`./bin/fsa ` / `dart run artisan `) once the project has run `dusk:install`. Commands are auto-discovered through `DuskArtisanProvider`; nothing wires by hand. Need a quick reminder of what a command does without leaving the terminal? Run `dart run artisan list` for the full registry grouped by namespace, or `dart run artisan help ` for the per-command flag surface. This page exists for the deeper view: the boot mode, the backing VM Service extension, and the grouping rationale. ## Table of contents - [Snapshot and screenshot](#snapshot-and-screenshot) - [Gestures](#gestures) - [Inputs](#inputs) - [Navigation](#navigation) - [Find](#find) - [Diagnostics](#diagnostics) - [Install](#install) - [CDP](#cdp) - [Click variants](#click-variants) - [Focus and blur](#focus-and-blur) - [Console and exceptions](#console-and-exceptions) - [Observe](#observe) - [Hot reload and snap](#hot-reload-and-snap) ## How to read this page Each group section ships a single table with four columns: - **Command** is the canonical name you type after `dart run fluttersdk_dusk` (or via the artisan dispatcher). - **Description** is the one-line summary returned by the command's `description` getter; the same string surfaces in `dart run artisan list`. - **Boot Mode** is the `CommandBoot` value the dispatcher reads before invoking `handle()`. `none` means pure CLI: no VM Service connection. `connected` means the command dials `~/.artisan/state.json` and fails fast if no app is running. - **VM Extension** is the `ext.dusk.*` method the command calls over the VM Service. `none` for commands that operate purely on the consumer filesystem. Deep-dive pages exist for the seven commands whose flag surface, return shape, or composition rules outgrow a single table row. The remaining twenty-five commands share this index page; reach for `dart run artisan help ` for their full flag surface. ## Snapshot and screenshot The two foundational read commands. `dusk:snap` walks the Semantics tree and emits YAML with `[ref=eN]` tokens that every subsequent action command consumes. `dusk:screenshot` captures the rendered pixel buffer over Chrome DevTools Protocol (web) or `RepaintBoundary.toImage` (desktop, mobile). | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:snap`](dusk-snap.md) | Capture Semantics tree YAML of the running Flutter app with [ref=eN] tokens. | connected | ext.dusk.snap | | [`dusk:screenshot`](dusk-screenshot.md) | Capture a screenshot of the running Flutter app to a file. | connected | ext.dusk.screenshot | ## Gestures Pointer-driven actions that synthesise touch, mouse, or pen events at a widget located by ref token. All four route through the actionability gate (enabled, zero-rect, off-viewport) before the event leaves the VM. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:tap`](dusk-tap.md) | Tap a widget by ref token (from prior dusk:snap). | connected | ext.dusk.tap | | `dusk:hover` | Hover the pointer over a widget by ref token (from prior dusk:snap). | connected | ext.dusk.hover | | `dusk:drag` | Drag from one widget to another using ref tokens from a prior dusk:snap. | connected | ext.dusk.drag | | `dusk:scroll` | Scroll inside a scrollable widget by ref token from a prior dusk:snap. | connected | ext.dusk.scroll | ## Inputs Keyboard-shaped actions. `dusk:type` emits a character sequence; `dusk:press_key` synthesises a single hardware-key event; `dusk:clear` empties the focused text field; `dusk:select_option` drives `DropdownButton` / `PopupMenuButton`; `dusk:set_checkbox` drives `Checkbox` / `Switch`. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:type` | Type text into a focused widget by ref token. | connected | ext.dusk.type | | `dusk:press_key` | Synthesise a hardware-key event on the currently focused widget. | connected | ext.dusk.press_key | | `dusk:clear` | Empty the text content of the focused widget by ref. | connected | ext.dusk.clear | | `dusk:select_option` | Select an option in a DropdownButton or PopupMenuButton by ref token. | connected | ext.dusk.select_option | | `dusk:set_checkbox` | Set the checked state of a Checkbox or Switch widget by ref. | connected | ext.dusk.set_checkbox | | `dusk:wait` | Wait for a text, text-gone, or expression condition in the running app. | connected | ext.dusk.wait_for | | `dusk:wait_for_network_idle` | Wait until the running app reports zero in-flight HTTP requests for a contiguous idleMs window. | connected | ext.dusk.wait_for_network_idle | ## Navigation Route-table manipulation against the active `Navigator`. `dusk:modal` dismisses any open modal, sheet, or dialog. `dusk:close_app` ends the session via `SystemNavigator.pop()`. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:navigate` | Navigate the running app to a named route via the active Navigator. | connected | ext.dusk.navigate | | `dusk:navigate_back` | Pop the topmost route off the active Navigator (mirrors browser back). | connected | ext.dusk.navigate_back | | `dusk:get_routes` | Print the active Navigator's route table + current location as JSON. | connected | ext.dusk.get_routes | | `dusk:modal` | Dismiss all open modals, bottom sheets, and dialogs in the running app. | connected | ext.dusk.dismiss_modals | | `dusk:close_app` | Gracefully close the running app via SystemNavigator.pop(). | connected | ext.dusk.close_app | ## Find The Playwright Locator surface: mint a re-resolvable `q` handle backed by text, semanticsLabel, or key predicates. Every action call against a `q` handle re-walks the live tree, so the handle survives intermediate rebuilds. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:find`](dusk-find.md) | Mint a re-resolvable q-handle by text / semanticsLabel / key (Playwright Locator pattern). | connected | ext.dusk.find | ## Diagnostics Pure-CLI checks that verify the consumer wiring and the running session health. Neither command dials the VM Service. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:doctor`](dusk-doctor.md) | Verify dusk plugin runtime + consumer wiring health | none | none | ## Install The one-shot bootstrap. Injects three lines into the consumer's `lib/main.dart` and is otherwise idempotent. No `bin/artisan.dart` scaffold; `fluttersdk_dusk` ships its own Flutter-free CLI entry point. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:install`](dusk-install.md) | Wire DuskPlugin.install() into lib/main.dart AND chain artisan install + plugin:install so ./bin/fsa surfaces all 32 dusk:* commands (idempotent on re-run; Phase 2 chain is best-effort). | none | none | ## CDP Chrome DevTools Protocol commands that manipulate the browser viewport directly. Web target only; desktop and mobile no-op gracefully. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:device` | Emulate a device profile (viewport + DPR + touch + user agent) via Chrome DevTools Protocol. | connected | none (CDP direct) | | `dusk:resize` | Resize the running Flutter web app viewport via Chrome DevTools Protocol. | connected | none (CDP direct) | ## Click variants Pointer gestures that go beyond the primary tap. All four route through the actionability gate. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:dblclick` | Fire a double-click at the widget identified by a snapshot ref. | connected | ext.dusk.tap | | `dusk:triple_click` | Fire three primary clicks (~100ms apart) at the widget identified by --ref. | connected | ext.dusk.tap | | `dusk:right_click` | Fire a right (secondary mouse button) click at the widget identified by --ref. | connected | ext.dusk.tap | ## Focus and blur Keyboard-focus shaping. `dusk:focus` requests focus on a ref; `dusk:blur` releases the currently focused widget. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:focus` | Request keyboard focus on the widget identified by --ref. | connected | ext.dusk.focus | | `dusk:blur` | Remove keyboard focus from the currently-focused widget. | connected | ext.dusk.blur | ## Console and exceptions Telescope-backed readers. The running app must have `fluttersdk_telescope` wired for these to return entries; otherwise they emit an empty buffer. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:console` | Read recent log entries from the running app's telescope store. | connected | ext.telescope.logs | | `dusk:exceptions` | Read recent exception entries from the running app's telescope store. | connected | ext.telescope.exceptions | ## Observe The Stagehand observe-once-act-many surface. Returns a structured candidate list of every interactive widget on screen; the agent decides which refs to act on. No server-side LLM. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | [`dusk:observe`](dusk-observe.md) | Return a structured candidate list of every interactive widget on screen (Stagehand observe-once-act-many; no server-side LLM). | connected | ext.dusk.observe | ## Hot reload and snap The single-round-trip composite that hot-reloads the running app and then captures snapshot, screenshot, and recent exceptions in one shot. Dispatched via the `artisan:` substrate routing prefix to avoid a same-isolate deadlock. | Command | Description | Boot Mode | VM Extension | |---------|-------------|-----------|--------------| | `dusk:hot_reload_and_snap` | Hot reload the running app, then capture snapshot + screenshot + recent exceptions in a single round-trip. | connected | artisan:reload + ext.dusk.snap | ## Boot mode and deep-dives Two of thirty-two commands run with `CommandBoot.none` (`dusk:install`, `dusk:doctor`). Every other command is `CommandBoot.connected`: it dials the VM Service URI in `~/.artisan/state.json` and fails fast when the running app cannot be reached. Seven commands earn their own pages: [dusk:install](dusk-install.md), [dusk:snap](dusk-snap.md), [dusk:tap](dusk-tap.md), [dusk:screenshot](dusk-screenshot.md), [dusk:find](dusk-find.md), [dusk:doctor](dusk-doctor.md), [dusk:observe](dusk-observe.md). Slug rule: the URL replaces the `:` separator with `-`. The remaining twenty-five commands share this index page; reach for `dart run artisan help ` for their full flag surface. --- # dusk:install # dusk:install One-shot bootstrap for `fluttersdk_dusk`. Runs in two phases, both idempotent: 1. **Patch `lib/main.dart`** — inject the imports, `WidgetsFlutterBinding.ensureInitialized()`, and a `kDebugMode`-gated `DuskPlugin.install()` block before `runApp(`. Magic-stack apps additionally wire `MagicDuskIntegration.install()` after `Magic.init(`. 2. **Chain fastcli setup** — best-effort `dart run fluttersdk_dusk install` (scaffolds `bin/dispatcher.dart` + `./bin/fsa` AOT wrapper) + `dart run fluttersdk_dusk plugin:install fluttersdk_dusk` (registers `DuskArtisanProvider`). Both sub-process calls are skipped when their idempotency markers already exist (`bin/dispatcher.dart` for the scaffold, `.artisan/installed/fluttersdk_dusk.json` for the plugin record). Failures are swallowed with a warning; the Phase 1 patch always succeeds on its own, so the consumer can still drive dusk via `dart run fluttersdk_dusk ` even when the chain skipped. Together, the two phases mean a fresh consumer needs only: ```bash flutter pub add fluttersdk_dusk dart run fluttersdk_dusk dusk:install ``` After the second command, `./bin/fsa list` surfaces all 32 `dusk:*` commands and `./bin/fsa mcp:serve` exposes the 31 MCP tools. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Anchor modes](#anchor-modes) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:install ``` `dusk:install` accepts no positional arguments and no flags. The command reads `lib/main.dart` and `pubspec.yaml` from the current working directory and injects the runtime wiring snippets that fit the detected stack. --- ## Arguments `dusk:install` has no `addOption` or `addFlag` calls in its `configure` method. The two side-channel inputs are environment-derived: | Input | Source | Purpose | |-------|--------|---------| | `lib/main.dart` path | `DuskInstallCommand.mainDartPathResolver()` (defaults to `lib/main.dart`) | Target file for snippet injection. Test seam: override the resolver to point at a fixture. | | `pubspec.yaml` path | `DuskInstallCommand.pubspecPathResolver()` (defaults to `pubspec.yaml`) | Inspected for `magic:` and `fluttersdk_wind:` dependency entries. Drives the conditional wiring decisions described under [Anchor modes](#anchor-modes). | Both resolvers are public static fields so tests can override per-test without leaking files into the running test process' cwd. --- ## Returns `dusk:install` returns an integer exit code via `Future`: | Exit code | Meaning | |-----------|---------| | `0` | Success. `lib/main.dart` was either updated with the required snippets or already contained them. The command prints a `dusk:install complete` success line. | | `1` | `lib/main.dart` not found at the resolved path. The command prints an error advising the operator to run `dusk:install` from a Flutter project root. | No structured payload is emitted. Status flows through `ArtisanOutput.info` / `success` / `error` so it surfaces with the same `[info]` / `[ok]` / `[error]` tokens as every other artisan command. --- ## Anchor modes The injector picks one of two anchor strings depending on what `lib/main.dart` already contains: - **Magic-stack apps** (`lib/main.dart` contains `await Magic.init(`): `DuskPlugin.install()` is wired BEFORE `Magic.init(` so the driver is live during Magic boot. When `magic:` is also a pubspec dependency, `MagicDuskIntegration.install()` is also injected AFTER `Magic.init()` (the integration queries `Magic.find()` for the form and nav enrichers, which only resolves once the container is ready). - **Vanilla apps** (no `Magic.init` anchor): `DuskPlugin.install()` is wired immediately before `runApp(`. When the consumer's pubspec lists `fluttersdk_wind:` as a top-level dependency, `Wind.installDebugResolver()` lands inside the same `kDebugMode` block as `DuskPlugin.install()`. Wind alpha-10 no longer ships a dusk-specific integration class; dusk reads wind state through the neutral `WindDebugRegistry` bridge at snap time. The Wind enricher wiring is independent of the Magic detection: a magic-free app with `fluttersdk_wind` still gets the wind metadata block. The full sub-step list (from the source docblock): 1. Add the two required imports (`kDebugMode` from `package:flutter/foundation.dart`; the `package:fluttersdk_dusk/dusk.dart` barrel). 2. Inject `WidgetsFlutterBinding.ensureInitialized()` (skip when already present) plus the `kDebugMode`-gated dusk block before the canonical install anchor. 3. When pubspec has `magic:` AND main.dart has `await Magic.init(`, inject `MagicDuskIntegration.install()` AFTER that call. --- ## Examples ### 1. Fresh install in a vanilla Flutter app ```bash flutter create my_app cd my_app dart run fluttersdk_dusk dusk:install ``` Expected output (illustrative): ``` [info] Wiring DuskPlugin into lib/main.dart... [ok] dusk:install complete. Run `dart run fluttersdk_dusk ` to invoke dusk commands. ``` Diff against `lib/main.dart`: ```dart import 'package:flutter/foundation.dart' show kDebugMode; import 'package:fluttersdk_dusk/dusk.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); if (kDebugMode) { DuskPlugin.install(); } runApp(const MyApp()); } ``` ### 2. Re-running on an already-installed project ```bash dart run fluttersdk_dusk dusk:install ``` The injector early-returns on every duplicate snippet. The output still prints `dusk:install complete`; the file is left untouched. ### 3. Magic-stack app with wind enricher When pubspec lists both `magic:` and `fluttersdk_wind:`, the post-install `lib/main.dart` looks like: ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); if (kDebugMode) { DuskPlugin.install(); Wind.installDebugResolver(); } await Magic.init(MyApp.new); if (kDebugMode) { MagicDuskIntegration.install(); } } ``` --- ## See also - [Getting Started: Installation](../getting-started/installation.md): step-by-step walkthrough from `dart pub add` to first snapshot. - [dusk:doctor](dusk-doctor.md): verify the post-install wiring is healthy and the running session can be reached. - [Plugins: Magic integration](../plugins/magic-integration.md): full surface of `MagicDuskIntegration` and which enrichers it ships. - [Plugins: Wind integration](../plugins/wind-integration.md): the six-field `wind:` block surfaced through the neutral `WindDebugRegistry` bridge in wind alpha-10. --- # dusk:snap # dusk:snap Capture the Semantics tree of the running Flutter app as YAML, tagging every interactive node with a `[ref=eN]` token. `dusk:snap` is the foundational read command: every action command (`dusk:tap`, `dusk:type`, `dusk:drag`, etc.) consumes one of its `eN` refs to locate the target widget. The `eN` namespace is snapshot-frozen: every fresh `dusk:snap` clears the registry and mints new tokens. For long-lived handles that survive rebuilds, use `dusk:find` (which mints `qN` tokens) or `dusk:observe`. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Enricher fragments](#enricher-fragments) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:snap [--depth=] [--includeEnrichers] ``` `dusk:snap` requires a running Flutter session (`CommandBoot.connected`). It dials the VM Service URI recorded in `~/.artisan/state.json`, calls `ext.dusk.snap`, and prints the snapshot YAML to stdout. --- ## Arguments | Option | Type | Default | Description | |--------|------|---------|-------------| | `--depth` | int (string-parsed) | unset (full tree) | Optional max tree depth. Caps the walk so very deep widget trees stay readable. Omit for the full tree. | | `--includeEnrichers` | flag | `false` | Emit Magic and Wind enricher fragments under each ref entry. Default off matches the Playwright-style minimal snapshot; turn on when the agent needs the className tokens, route name, or form field metadata. | The flag is parsed via `(ctx.input.option('includeEnrichers') as bool?) ?? false` and serialised as a string into the VM Service params map. --- ## Returns The VM Service handler returns a JSON envelope `{ "snapshot": "" }`. The CLI unwraps the `snapshot` field and writes the raw YAML to stdout; when the field is missing the entire JSON object is dumped instead. **Success envelope (illustrative):** ```yaml [ref=e1] role=button label="Sign in" rect=(120,400,120,48) actions=[tap] [ref=e2] role=textbox label="Email" rect=(20,200,335,56) actions=[tap, focus, type] [ref=e3] role=text label="Welcome back" rect=(20,80,335,32) ``` When `--includeEnrichers` is set, each entry gains indented lines contributed by the registered enrichers (see [Enricher fragments](#enricher-fragments)). **Error envelope:** The VM Service handler propagates errors as `ServiceExtensionResponse.error(extensionError, message)`. The CLI surfaces the exception via `ArtisanContext.callExtension` and exits with a non-zero status. Typical failure modes: - No running app at the recorded URI (the artisan dispatcher reports the dial failure before `dusk:snap` runs). - `DuskPlugin.install()` not wired in `lib/main.dart` (run `dusk:install`). --- ## Enricher fragments When `--includeEnrichers` is true, every `DuskSnapshotEnricher` appended to the `DuskPlugin.enrichers` live list contributes indented lines under each ref. The two first-party enrichers: - **MagicDuskIntegration** ships seven enrichers covering forms, routes, controllers, models, http, cache, and the policy gate. - **Wind alpha-10** surfaces a six-field `wind:` block (breakpoint, brightness, platform, states, bgColor, textColor) through the neutral `fluttersdk_wind_diagnostics_contracts.WindDebugRegistry` bridge rather than via an enricher, so the data appears under each W-prefixed widget ref without any enricher registration. Each enricher is synchronous and stateless; the `Element` reference is never retained across calls. --- ## Examples ### 1. Minimal snapshot of the current screen ```bash dart run fluttersdk_dusk dusk:snap ``` Expected output (illustrative; truncated): ```yaml [ref=e1] role=text label="Monitors" rect=(20,80,200,32) [ref=e2] role=button label="New monitor" rect=(20,140,335,48) actions=[tap] [ref=e3] role=button label="Settings" rect=(355,140,40,48) actions=[tap] ``` ### 2. Snapshot with depth cap ```bash dart run fluttersdk_dusk dusk:snap --depth=4 ``` Caps the walk at four levels of nesting. Useful for very dense screens (long lists, complex forms) where the full tree is noisy. ### 3. Snapshot with enrichers turned on ```bash dart run fluttersdk_dusk dusk:snap --includeEnrichers ``` Expected output (illustrative): ```yaml [ref=e2] role=button label="New monitor" rect=(20,140,335,48) actions=[tap] magicRoute: /monitors windClassName: bg-primary-600 dark:bg-primary-500 text-white ``` --- ## See also - [dusk:tap](dusk-tap.md): consume an `eN` token to synthesise a tap. - [dusk:find](dusk-find.md): mint a long-lived `qN` handle that survives rebuilds. - [dusk:observe](dusk-observe.md): structured candidate list when the agent needs more than the raw Semantics tree. - [Plugins: Enricher authoring](../plugins/enricher-authoring.md): how to ship a custom enricher that contributes additional fragments. --- # dusk:tap # dusk:tap Synthesise a tap at the widget identified by a snapshot ref token. `dusk:tap` is the primary action command: every interactive flow in an agent-driven test starts with a `dusk:snap` (to obtain `eN` refs) followed by one or more `dusk:tap --ref=` calls. The handler routes through the actionability gate (enabled, zero-rect, off-viewport) before the pointer event leaves the VM. A gate failure throws `DuskActionabilityException` and surfaces as a structured error; the agent re-snaps or re-finds rather than silently retrying. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Actionability gate](#actionability-gate) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:tap --ref= [--includeSnapshot] [--[no-]checkStable] [--[no-]checkReceivesEvents] ``` `dusk:tap` requires a running Flutter session (`CommandBoot.connected`). It dials the VM Service URI, calls `ext.dusk.tap`, and prints either a one-line success or the post-tap snapshot JSON depending on `--includeSnapshot`. --- ## Arguments | Option | Type | Default | Required | Description | |--------|------|---------|----------|-------------| | `--ref` | string | (none) | yes (`mandatory: true`) | Snapshot ref token (e.g. `e1` from a prior `dusk:snap`, or `q1` from a prior `dusk:find`). Empty values trigger a CLI-side error with exit code `1` before the VM Service call is made. | | `--includeSnapshot` | flag | `false` | no | Embed the post-tap snapshot YAML in the response. Useful when the tap is expected to trigger a navigation or modal and the next agent step needs the fresh tree. | | `--checkStable` | flag | `true` | no | Run the Stable (2-frame rect-unchanged) actionability gate. Disable when targeting an animating widget that intentionally rebuilds across frames. | | `--checkReceivesEvents` | flag | `true` | no | Run the Receives-Events (front-most hit-test) actionability gate. Disable when targeting a widget that is intentionally occluded by an overlay you also want to interact with. | The two `check*` flags default to `true`. Disable them with the inverted form (`--no-checkStable`, `--no-checkReceivesEvents`) when the target genuinely should not be subject to that precondition. --- ## Returns `dusk:tap` returns an integer exit code via `Future`: | Exit code | Meaning | |-----------|---------| | `0` | Tap synthesised. The handler emits either `Tapped ` (default) or the full JSON envelope when `--includeSnapshot` is set. | | `1` | `--ref` was missing or empty (CLI-side guard fires before the VM Service call). | | non-zero | VM Service handler returned `ServiceExtensionResponse.error`. Common causes: ref not found in the registry, actionability gate failure, no running app at the recorded URI. | **Success envelope (default, `--includeSnapshot` off):** ``` [ok] Tapped e2 ``` **Success envelope (`--includeSnapshot` on):** ```json { "snapshot": "[ref=e1] role=text label=\"Welcome\" ...", "ref": "e2" } ``` **Error envelope (actionability gate failure):** The message format is `Widget ref= is not actionable: ` where `` is one of `defunct (...)`, `not enabled`, `zero rect`, `off-viewport (rect=..., viewport=...)`, `not stable (rect changed by Xpx)`, or `obscured by other widget (top=...)`. The agent branches by substring-matching on ``; the full vocabulary is documented under [Reference: Actionability gate](../reference/actionability-gate.md#failure-reason-substrings). --- ## Actionability gate The actionability gate runs six preconditions in order: defunct (Step 0, preflight: render-object still attached), enabled (Tristate.isFalse fails), zero-rect (zero width or height), off-viewport (auto-`showOnScreen` first when a Scrollable ancestor exists), stable (2-frame rect drift ≤ 0.5px; opt out via `--no-checkStable`), and receives-events (hit-test path includes the target or descendant; opt out via `--no-checkReceivesEvents`). The full reference lives at [Reference: Actionability gate](../reference/actionability-gate.md). Two further checks layer on top when their CLI flags are enabled: - `checkStable` ; the rect is unchanged across two consecutive frames. - `checkReceivesEvents` ; the hit-test at the rect center resolves to the target widget (i.e. no overlay is intercepting the tap). See [Reference: Actionability gate](../reference/actionability-gate.md) for the full ordering rationale and how new preconditions can be appended without breaking pinned agent prompts. --- ## Examples ### 1. Tap a button by its snapshot ref ```bash dart run fluttersdk_dusk dusk:snap > /tmp/snap.yaml # locate the button's eN in /tmp/snap.yaml dart run fluttersdk_dusk dusk:tap --ref=e2 ``` Expected output (illustrative): ``` [ok] Tapped e2 ``` ### 2. Tap and capture the post-tap tree in one round trip ```bash dart run fluttersdk_dusk dusk:tap --ref=e2 --includeSnapshot ``` Expected output (illustrative; abbreviated): ```json {"snapshot":"[ref=e1] role=text label=\"Detail page\" ...","ref":"e2"} ``` ### 3. Tap an intentionally-animating widget ```bash dart run fluttersdk_dusk dusk:tap --ref=e5 --no-checkStable ``` Skips the 2-frame stable check for an animating loader / shimmer / spinner that the agent legitimately wants to interact with. --- ## See also - [dusk:snap](dusk-snap.md): produce the `eN` refs that `dusk:tap` consumes. - [dusk:find](dusk-find.md): mint a re-resolvable `qN` handle that survives rebuilds; pass it as `--ref=q1`. - [dusk:observe](dusk-observe.md): structured candidate list when the agent needs more than the raw Semantics tree. - [Reference: Actionability gate](../reference/actionability-gate.md): full ordering and message format the agent branches on. --- # dusk:screenshot # dusk:screenshot Capture a screenshot of the running Flutter app to a file. The VM Service handler walks the render tree to find the `RepaintBoundary` that `DuskPlugin.install()` wrapped around the app root, calls `toImage`, and returns a base64-encoded PNG or JPEG. The CLI decodes the base64 and writes the bytes to the path supplied by `--output`. JPEG is the default for size: a typical Flutter web screen renders in ~30 KB at quality 70. Switch to PNG when the agent needs lossless pixels for a diff comparison. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Format and quality](#format-and-quality) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:screenshot --output= [--format=jpeg|png] [--quality=<1-100>] ``` `dusk:screenshot` requires a running Flutter session (`CommandBoot.connected`). It dials the VM Service URI, calls `ext.dusk.screenshot`, decodes the base64 payload, and writes the resulting bytes to the supplied output path. --- ## Arguments | Option | Abbr | Type | Default | Required | Description | |--------|------|------|---------|----------|-------------| | `--output` | `-o` | string (path) | (none) | yes (`mandatory: true`) | Output file path. Resolved relative to the CWD. The directory must already exist. | | `--format` | none | enum | `jpeg` | no | One of `jpeg`, `png`. Constrained by `allowed: ['jpeg', 'png']` so any other value errors out at parse time. | | `--quality` | none | int (string-parsed) | `70` | no | JPEG quality, range 1-100. Ignored for PNG. Falls back to `70` when the value fails `int.tryParse`. | The `--output` guard fires before the VM Service call; an empty or missing path returns exit code `1` with `Missing --output=.`. --- ## Returns `dusk:screenshot` returns an integer exit code via `Future`: | Exit code | Meaning | |-----------|---------| | `0` | Screenshot captured and written. The handler emits `Wrote base64 chars to ` where `` is the length of the base64 string before decoding (useful for spotting empty buffers without inspecting the file). | | `1` | `--output` was missing or empty (CLI-side guard); OR the VM Service handler returned a response without a `base64` field. | **Success envelope (CLI side):** ``` [ok] Wrote 41268 base64 chars to /tmp/dashboard.jpeg ``` **VM Service envelope (handler side):** ```json { "base64": "" } ``` The CLI calls `base64Decode(base64Str)` and writes the resulting bytes to `output`. No additional metadata (width, height, mime) is returned today; the agent infers shape from the file on disk if needed. --- ## Format and quality - **jpeg** (default): the VM Service handler encodes via the `image` package's JPEG encoder at the supplied quality. Quality 70 is a Playwright-aligned default; bump to 90 for visual-regression diffs, drop to 40 for quick sanity checks. - **png**: lossless, larger files (typically 5x JPEG at quality 70). The `--quality` value is ignored. The handler never resizes the screenshot; the captured image matches the running viewport's pixel dimensions (logical size times DPR). To capture at a controlled viewport, run `dusk:resize` or `dusk:device` first. --- ## Examples ### 1. Capture the current screen as JPEG ```bash dart run fluttersdk_dusk dusk:screenshot --output=/tmp/screen.jpeg ``` Expected output (illustrative): ``` [ok] Wrote 41268 base64 chars to /tmp/screen.jpeg ``` ### 2. Capture losslessly for a visual diff ```bash dart run fluttersdk_dusk dusk:screenshot --output=/tmp/screen.png --format=png ``` Useful when the agent compares against a baseline PNG via `image_diff` or `pixelmatch`. ### 3. Capture at high quality JPEG ```bash dart run fluttersdk_dusk dusk:screenshot --output=/tmp/hifi.jpeg --quality=92 ``` ### 4. Capture after a controlled resize ```bash dart run fluttersdk_dusk dusk:resize --width=1440 --height=900 dart run fluttersdk_dusk dusk:screenshot --output=/tmp/desktop.jpeg ``` --- ## See also - [dusk:snap](dusk-snap.md): the structured-text counterpart; agents typically pair `dusk:snap` and `dusk:screenshot` to read both the Semantics tree and the pixels in a single round. - [dusk:hot_reload_and_snap](index.md#hot-reload-and-snap): captures snapshot + screenshot + recent exceptions in one round trip. - [dusk:resize](index.md#cdp) and [dusk:device](index.md#cdp): control the viewport that `dusk:screenshot` captures from on web targets. --- # dusk:find # dusk:find Mint a re-resolvable `q` handle backed by one or more predicates (text, semanticsLabel, key). Mirrors Playwright's Locator semantics: every action call against a `qN` handle re-walks the live Semantics tree on each invocation, so the handle survives intermediate widget rebuilds without going stale. `qN` and `eN` token spaces are disjoint. A handle minted by `dusk:find` is always `qN`; a handle harvested from `dusk:snap` is always `eN`. The dispatcher distinguishes by prefix, but action commands accept either shape via `--ref=`. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Re-resolution semantics](#re-resolution-semantics) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:find [--text=] [--contains=] [--semanticsLabel=] [--key=] ``` `dusk:find` requires a running Flutter session (`CommandBoot.connected`). It dials the VM Service URI, calls `ext.dusk.find` with the supplied predicates, and prints the minted handle envelope as pretty-printed JSON. At least one of the four options must be non-empty; an empty params map returns exit code `1` with a CLI-side error before the VM Service call. --- ## Arguments | Option | Type | Default | Description | |--------|------|---------|-------------| | `--text` | string | unset | Exact match against the widget's visible text label (the Semantics `value` or rendered `Text` content). Most common predicate; mirrors Playwright's `getByText` with exact-match semantics. | | `--contains` | string | unset | Substring match against the visible text label or Semantics label (case-sensitive). Use when the label is dynamic (counters, timestamps, plurals) and exact `--text` is too brittle. | | `--semanticsLabel` | string | unset | Exact match against the widget's accessibility label (the explicit `Semantics(label: ...)` value set by the widget tree). Use when the visible text and the a11y label diverge. | | `--key` | string | unset | Match the widget's `ValueKey` identifier (the `Key('signin-button')` form). Most precise; survives label and copy changes. | The predicates compose AND: a `dusk:find --text=Sign --key=signin-button` call returns the widget that matches both. Use a single predicate when the agent only needs one axis. The CLI guards an empty params map (`Provide at least one of --text / --contains / --semanticsLabel / --key.`) so the VM Service handler never sees a zero-predicate call. --- ## Returns `dusk:find` returns an integer exit code via `Future`: | Exit code | Meaning | |-----------|---------| | `0` | Handle minted (or re-used; the registry is content-addressed). The handler emits the JSON envelope below. | | `1` | No predicate supplied. The CLI guard fires before the VM Service call. | | non-zero | VM Service handler returned `ServiceExtensionResponse.error`. Typical cause: the predicate matched zero widgets (no matches surfaces as a structured failure so the agent knows to broaden the predicate). | **Success envelope (illustrative):** ```json { "ref": "q1", "matchCount": 1, "rect": [120, 400, 240, 48], "role": "button", "label": "Sign in" } ``` `matchCount > 1` indicates the predicate is ambiguous: the handle still resolves to the first match, but the agent should narrow with an extra predicate (typically `--key`) before acting. **Error envelope:** The VM Service handler propagates errors as `ServiceExtensionResponse.error(extensionError, message)`. The CLI surfaces them via `ArtisanContext.callExtension` and exits non-zero. Common messages include `No widget matched predicates: {...}`. --- ## Re-resolution semantics A `qN` handle stores the predicate map, not the matched widget. Every action call (`dusk:tap --ref=q1`, `dusk:type --ref=q1`, etc.) re-executes the query against the live Semantics tree. Three consequences: 1. **Widget rebuilds don't invalidate the handle.** A `ListView` swap, a navigation transition, or a state-driven rebuild all leave the handle valid as long as the predicates still match something. 2. **The query is re-run on every action.** Cheap (a Semantics walk on each call), but emergent if the predicate is broad: prefer `--key` over `--text` for hot paths. 3. **`dusk:find` itself is idempotent in the registry.** Calling it twice with the same predicate map returns the same `qN`; the registry is content-addressed. `eN` handles minted by `dusk:snap` work the opposite way: they freeze the matched widget at snap time and go stale on the next rebuild. Use `eN` for one-shot reads, `qN` for any sequence that spans more than one frame. --- ## Examples ### 1. Mint a handle by visible text ```bash dart run fluttersdk_dusk dusk:find --text="Sign in" ``` Expected output (illustrative): ```json { "ref": "q1", "matchCount": 1, "rect": [120, 400, 240, 48], "role": "button", "label": "Sign in" } ``` Reuse `q1` across subsequent action calls: ```bash dart run fluttersdk_dusk dusk:tap --ref=q1 ``` ### 2. Mint a handle by accessibility label ```bash dart run fluttersdk_dusk dusk:find --semanticsLabel="Submit form" ``` Use when the rendered button text is an icon and the only stable predicate is the a11y label. ### 3. Mint a handle by widget key (most precise) ```bash dart run fluttersdk_dusk dusk:find --key="signin-submit" ``` Survives copy changes and a11y-label changes. Pair with a widget-side `Key('signin-submit')` declaration. ### 4. Mint a handle by substring (dynamic label) ```bash dart run fluttersdk_dusk dusk:find --contains="pushed the button" ``` Useful when the visible label is dynamic, e.g. `"You have pushed the button 5 times:"` (counter changes per tap). `--text` would only match the exact string at the moment of capture; `--contains` survives the counter advancing. ### 5. Compose two predicates to disambiguate ```bash dart run fluttersdk_dusk dusk:find --text="Save" --key="monitor-form-save" ``` The two predicates AND together; useful when the screen has multiple "Save" buttons but only one with the canonical key. --- ## See also - [dusk:snap](dusk-snap.md): produce `eN` refs for one-shot reads. - [dusk:tap](dusk-tap.md): consume the `qN` ref to synthesise a tap. - [dusk:observe](dusk-observe.md): structured candidate list of every interactive widget; useful when the agent doesn't know which predicate to query. --- # dusk:observe # dusk:observe Return a structured candidate list of every interactive widget on screen. Mirrors the Stagehand observe-once-act-many pattern: the agent observes once, then issues many `dusk:tap` / `dusk:type` / `dusk:drag` calls against the minted `qN` refs without re-observing between actions. No LLM is invoked server-side. The handler walks the live Semantics tree, mints a re-resolvable `qN` handle for each interactive widget, and returns the candidate list as JSON. The agent reads the list and decides which refs to act on. This is what differentiates `dusk:observe` from a model-side `dusk:snap`: it returns a flat, role-filterable list optimised for LLM consumption rather than the full tree. The CLI surface is mostly for debugging; the MCP descriptor is the primary surface for agent integrations. --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [Observe-once-act-many](#observe-once-act-many) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:observe [--intent=] [--roles=] [--limit=] [--includeEnrichers=] ``` `dusk:observe` requires a running Flutter session (`CommandBoot.connected`). It dials the VM Service URI, calls `ext.dusk.observe`, and prints the JSON candidate list to stdout. --- ## Arguments | Option | Type | Default | Description | |--------|------|---------|-------------| | `intent` | string | unset | Free-form caller hint describing what the agent is looking for. Echoed back in the response; NOT used server-side for ranking or filtering. Useful for logging and for telemetry that wants to correlate observes with the agent's downstream intent. | | `roles` | csv string | unset (every role) | Comma-separated role filter (e.g. `button,textbox,checkbox`). Omit for every role. Useful when the agent already knows it only cares about, say, form fields. | | `limit` | int (string) | `50` | Maximum number of candidates to return. The handler ranks by hit-test depth and returns the first N. | | `includeEnrichers` | enum string | `true` | One of `true` (default; subset of enricher fields), `false` (no enricher fields), `full` (every enricher field). Use `full` when the agent needs the complete className tokens, route metadata, and form-field shape; use `false` for the smallest payload. | All four options pass through to the VM Service handler as string values (no client-side parsing). Empty strings are dropped so the handler sees absent rather than empty when the caller omits an option. --- ## Returns The VM Service handler returns a JSON envelope; the CLI dumps it to stdout via `jsonEncode`. **Success envelope (illustrative; `includeEnrichers=true`, single candidate shown):** ```json { "intent": "find the sign in button", "candidates": [ { "ref": "q1", "role": "button", "label": "Sign in", "rect": [120, 400, 240, 48], "actions": ["tap"], "enrichers": { "windClassName": "bg-primary-600 text-white", "magicRoute": "/login" } } ], "totalMatches": 1 } ``` Every candidate ships with: - A re-resolvable `qN` handle (Playwright Locator semantics: every action call re-walks the tree). - The Semantics `role` (button, textbox, checkbox, link, etc.). - The Semantics `label` (visible text or explicit a11y label). - The widget `rect` as `[left, top, width, height]`. - The available `actions` list (typically a subset of `tap`, `focus`, `type`, `scroll`). - The `enrichers` map when `includeEnrichers` is `true` or `full`. **Error envelope:** The VM Service handler propagates errors as `ServiceExtensionResponse.error(extensionError, message)`. Common causes: no running app at the recorded URI, `DuskPlugin.install()` not wired. --- ## Observe-once-act-many The Stagehand pattern that gives `dusk:observe` its name: 1. **Observe once.** A single `dusk:observe` call enumerates the interactive surface of the current screen, mints `qN` handles, and returns them in one JSON payload. 2. **Act many.** The agent issues a sequence of `dusk:tap --ref=qN`, `dusk:type --ref=qN`, `dusk:set_checkbox --ref=qN`, etc. against the minted refs WITHOUT re-observing between actions. Each action re-resolves the `qN` handle against the live tree, so the refs survive intermediate rebuilds. The "no server-side LLM" property is the second half of the pattern: Stagehand-the-product runs an LLM server-side to rank candidates by intent. `dusk:observe` returns the raw candidate list and lets the agent's own LLM rank, so no model context is consumed on the server, and the response is deterministic. Re-observe only when: - The agent navigated to a new screen (the handles minted on the previous screen become stale matches). - The candidate set itself changes (e.g. a modal opens, a list grows, a tab switches). For incremental state changes on the same screen (clicking a button that disables another button, typing into a field that reveals a new form section), re-resolution on every action call is sufficient; no second `dusk:observe` is needed. --- ## Examples ### 1. Enumerate every interactive widget on the current screen ```bash dart run fluttersdk_dusk dusk:observe ``` Returns up to 50 candidates with a subset of enricher fields. Useful as the first call after a navigation to discover what is on screen. ### 2. Filter to a single role ```bash dart run fluttersdk_dusk dusk:observe --roles=button --limit=10 ``` Limits the response to up to 10 button candidates. Useful when the agent already knows the next action is a tap. ### 3. Observe followed by act-many ```bash dart run fluttersdk_dusk dusk:observe --roles=textbox,button > /tmp/observe.json # agent reads /tmp/observe.json, decides to type into q1 then tap q2 dart run fluttersdk_dusk dusk:type --ref=q1 --text="user@example.com" dart run fluttersdk_dusk dusk:tap --ref=q2 ``` No re-observe between the two actions; both `qN` handles re-resolve against the live tree on each call. --- ## See also - [dusk:snap](dusk-snap.md): the raw Semantics-tree YAML; richer than `dusk:observe` but with `eN` refs that go stale on rebuild. - [dusk:find](dusk-find.md): mint a single `qN` handle from a known predicate; pair with `dusk:observe` when the agent already knows what to look for. - [dusk:tap](dusk-tap.md), `dusk:type`, `dusk:drag`: the action commands that consume the `qN` refs minted by `dusk:observe`. --- # dusk:doctor # dusk:doctor Verify the `fluttersdk_dusk` runtime and the consumer-side wiring health in a single pure-CLI pass. `dusk:doctor` does not dial the VM Service; every check runs against the consumer's filesystem, the artisan state file (`~/.artisan/state.json`), and a small set of environment probes. Five lightweight checks run in order, each emitting one row via the `ArtisanOutput` facade (colored `[ok]` / `[warn]` / `[error]` / `[info]` tokens in TTY mode, plain text under buffered or null output). --- ## Table of contents - [Synopsis](#synopsis) - [Arguments](#arguments) - [Returns](#returns) - [The five checks](#the-five-checks) - [Examples](#examples) - [See also](#see-also) --- ## Synopsis ``` dart run fluttersdk_dusk dusk:doctor ``` `dusk:doctor` accepts no positional arguments and no flags. It runs five preflight checks against the cwd and exits with a status code derived from the single ERROR-class check (the Semantics tree probe). `CommandBoot.none`: no VM Service connection. The command can run before `artisan start` (some checks will downgrade to "Skipped (no Chrome attached)" but the command itself completes cleanly). --- ## Arguments `dusk:doctor` has no `addOption` or `addFlag` calls in its `configure` method. The probes are configurable via test seams (static fields on `DuskDoctorCommand`) so tests can override per-test, but no CLI surface drives them. Test seams (for completeness; not user-facing): | Seam | Default | Purpose | |------|---------|---------| | `stateFileReader` | `StateFile.read` | Reads `~/.artisan/state.json`. | | `chromePidProbe` | `captureChromePid` | Locates the live Chrome PID under a given parent PID. | | `processStartTimeProbe` | POSIX `ps -o lstart=` / Windows wmic | Reads a process's start time. | | `nowProvider` | `DateTime.now` | Wall-clock source. | | `semanticsEnabledProbe` | returns `true` | Reports whether the running app forced Semantics on. | | `duskDisableEnvReader` | reads `DUSK_DISABLE` compile-time constant | Detects the kill-switch env-var. | | `enrichersProbe` | returns `0` | Reports the count of registered enrichers. | | `mainDartPathResolver` | returns `lib/main.dart` | Resolves the consumer's main file. | | `mainDartReader` | reads bytes off disk | Returns the main.dart source. | --- ## Returns `dusk:doctor` returns an integer exit code via `Future`: | Exit code | Meaning | |-----------|---------| | `0` | Every check that can ERROR passed. WARN and INFO rows do not fail the doctor. | | `1` | The Semantics-tree probe (check 4) returned false. This is the only ERROR-class check; failure indicates `DuskPlugin.install()` did not run. | No structured JSON envelope is emitted; the output is row-per-check via `ArtisanOutput.success` / `warning` / `error` / `info`. --- ## The five checks ### 1. Hot-restart staleness Reads `~/.artisan/state.json`, locates the live Chrome PID via `captureChromePid`, and compares Chrome's `ps -o lstart=` start time against `state.json.startedAt`. Drift over 30 s means a hot-restart spawned a fresh Chrome after the CLI wrote `state.json`; the cached isolate id will be stale, so the check WARNs and asks the operator to restart the CLI. Downgrades to an INFO "Skipped" row when no state.json exists, no Chrome can be found, or the lstart probe fails (POSIX-only). ### 2. DUSK_DISABLE env-var Reads the compile-time `--dart-define=DUSK_DISABLE=` constant. Non-empty values WARN with the actual value echoed back so the operator can confirm where the kill switch came from (a stale `.env` export, a CI flag, etc.). Empty value passes silently with `[ok]`. ### 3. Enricher list non-empty Reads the count of registered `DuskSnapshotEnricher` instances. Zero means the consumer wired `DuskPlugin.install()` but did not install `MagicDuskIntegration` (and the Magic stack is not in use). Wind metadata flows through the neutral `WindDebugRegistry` bridge in wind alpha-10, so a wind-only app still shows a `wind:` block per ref even when this count is zero. WARN, never fail. Defaults to `0` in CLI context (the pure-Dart doctor cannot reach into Flutter without pulling `dart:ui`); the WARN row is the correct CLI-time outcome. ### 4. Semantics tree forced on (ERROR-class) Reports whether `RendererBinding.instance.semanticsEnabled` is true. The only check that can fail the doctor (exit code `1`). The default probe returns `true` unconditionally because the pure-Dart CLI cannot import `package:flutter/rendering.dart` without pulling `dart:ui`; the real-runtime check belongs to a future VM-Service-attached doctor invocation. ### 5. Magic-init detection (INFO-only) Reads `lib/main.dart` and reports one of three states: - `Magic-stack detected, integration wired` ; both `Magic.init(` and `MagicDuskIntegration.install` are present. - `Magic detected but MagicDuskIntegration missing` ; `Magic.init(` is present but the integration is not. Suggests re-running `dusk:install`. - `vanilla Flutter detected` ; no `Magic.init(` anchor. INFO only; never fails the doctor regardless of the consumer stack. --- ## Examples ### 1. Healthy magic-stack app ```bash dart run fluttersdk_dusk dusk:doctor ``` Expected output (illustrative): ``` [ok] hot-restart staleness: no drift detected (Chrome PID 51234) [ok] DUSK_DISABLE env-var: unset (runtime hooks active) [ok] snapshot enrichers: enrichers registered: 8 [ok] Semantics tree forced on: enabled [info] Magic-init detection: Magic-stack detected, integration wired ``` Exit code: `0`. ### 2. Pre-flight on a freshly-installed vanilla app ```bash dart run fluttersdk_dusk dusk:doctor ``` Expected output (illustrative): ``` [info] hot-restart staleness: Skipped (no Chrome attached) [ok] DUSK_DISABLE env-var: unset (runtime hooks active) [warn] snapshot enrichers: no enrichers registered; install Magic + Wind integrations for richer snapshots [ok] Semantics tree forced on: enabled [info] Magic-init detection: vanilla Flutter detected ``` Exit code: `0`. The doctor passes despite the WARN row. ### 3. Consumer forgot to run `dusk:install` The doctor cannot directly detect missing `DuskPlugin.install()` from CLI context, but check 5 will report `Magic detected but MagicDuskIntegration missing` when the magic-stack glue is absent. Re-run `dusk:install` to fix. --- ## See also - [dusk:install](dusk-install.md): the bootstrap command. Re-run `dusk:install` when check 5 reports missing integration glue. - [Reference: Actionability gate](../reference/actionability-gate.md): the runtime gate that snapshot consumers rely on; not checked by the doctor today. - [Getting Started: Installation](../getting-started/installation.md): full bring-up walkthrough; the doctor is the verification step. --- # Dusk MCP Overview # Dusk MCP Overview `fluttersdk_dusk` does not ship its own MCP server. It plugs into the substrate MCP server hosted by [`fluttersdk_artisan`](https://fluttersdk.com/artisan/mcp/overview) by exporting an `ArtisanServiceProvider` (`DuskArtisanProvider`) that contributes 31 MCP tool descriptors. When the consumer registers the provider in `bin/artisan.dart` (or via the auto-discovered `lib/app/_plugins.g.dart` barrel), the substrate MCP server picks up the dusk tools at `initialize` time and surfaces them alongside its own 10 substrate tools, so the AI client sees a single unified catalog. - [Substrate MCP server, dusk tool descriptors](#substrate-and-descriptors) - [The 31 dusk tools](#tool-catalog) - [Dispatch surfaces: `ext.dusk.*` vs. `artisan:dusk:*`](#dispatch-surfaces) - [Lifecycle: state file, lazy reconnect, snap-act loop](#lifecycle) - [Related](#related) --- ## Substrate MCP server, dusk tool descriptors The substrate MCP server is surfaced through the dusk wrapper binary. Running `dart run fluttersdk_dusk mcp:serve` speaks stdio JSON-RPC, reads `~/.artisan/state.json` to find the running Flutter app's VM Service URI, and collects every registered provider's `mcpTools()` list at boot (the wrapper forces `collectMcpTools: true` so all 31 dusk_* tools surface without needing a scaffolded fastcli binary). `DuskArtisanProvider.mcpTools()` returns 31 `McpToolDescriptor` instances; the substrate server registers each as a regular MCP tool and dispatches calls through the descriptor's declared `extensionMethod`. No additional server process is launched for dusk; one MCP endpoint, one server, plugin-extensible. This means every `.mcp.json` snippet that wires the substrate MCP server already gives the AI client access to the dusk tools. There is no separate `fluttersdk_dusk:mcp` binary to add, no second `cwd` to configure. See [setup.md](setup.md) for the per-client install matrix. Running `dart run fluttersdk_dusk mcp:serve` directly (without scaffolded fastcli) now surfaces the same 31 tools via the wrapper's automatic `collectMcpTools: true` flag. --- ## The 31 dusk tools `DuskArtisanProvider.mcpTools()` returns the following 31 descriptors. The list is sorted alphabetically; each link jumps to the per-tool entry in [tool-reference.md](tool-reference.md). | Tool | Purpose | |---|---| | [`dusk_blur`](tool-reference.md#dusk_blur) | Clear keyboard focus from whatever currently holds it. | | [`dusk_clear`](tool-reference.md#dusk_clear) | Empty the `TextEditingController` of a resolved text field. | | [`dusk_close_app`](tool-reference.md#dusk_close_app) | Request a graceful shutdown via `SystemNavigator.pop()`. | | [`dusk_console`](tool-reference.md#dusk_console) | Read recent log entries from the telescope store. | | [`dusk_dblclick`](tool-reference.md#dusk_dblclick) | Double-click a widget by ref. | | [`dusk_device_profile`](tool-reference.md#dusk_device_profile) | Emulate a named device profile via CDP. | | [`dusk_dismiss_modals`](tool-reference.md#dusk_dismiss_modals) | Pop every modal route above the first persistent route. | | [`dusk_drag`](tool-reference.md#dusk_drag) | Drag from one widget to another by ref. | | [`dusk_evaluate`](tool-reference.md#dusk_evaluate) | Evaluate a Dart expression in the running isolate. | | [`dusk_exceptions`](tool-reference.md#dusk_exceptions) | Read recent exceptions from the telescope store. | | [`dusk_find`](tool-reference.md#dusk_find) | Mint a re-resolvable `q` handle by text / label / key. | | [`dusk_focus`](tool-reference.md#dusk_focus) | Request keyboard focus on a widget by ref. | | [`dusk_get_routes`](tool-reference.md#dusk_get_routes) | List route paths declared by the running router. | | [`dusk_hot_reload_and_snap`](tool-reference.md#dusk_hot_reload_and_snap) | Hot reload, snap, screenshot, exceptions in one round-trip. | | [`dusk_hover`](tool-reference.md#dusk_hover) | Hover a mouse cursor over a widget by ref. | | [`dusk_navigate`](tool-reference.md#dusk_navigate) | Navigate to a route path. | | [`dusk_navigate_back`](tool-reference.md#dusk_navigate_back) | Pop the top route off the navigator stack. | | [`dusk_observe`](tool-reference.md#dusk_observe) | Structured candidate list of interactive widgets (Stagehand pattern). | | [`dusk_press_key`](tool-reference.md#dusk_press_key) | Press a hardware key with optional modifiers. | | [`dusk_resize_viewport`](tool-reference.md#dusk_resize_viewport) | Resize the web viewport via CDP. | | [`dusk_right_click`](tool-reference.md#dusk_right_click) | Fire a right (secondary mouse) click. | | [`dusk_screenshot`](tool-reference.md#dusk_screenshot) | Capture a screenshot of the running app. | | [`dusk_scroll`](tool-reference.md#dusk_scroll) | Scroll a Scrollable widget by ref. | | [`dusk_select_option`](tool-reference.md#dusk_select_option) | Select an option in a DropdownButton. | | [`dusk_set_checkbox`](tool-reference.md#dusk_set_checkbox) | Read + conditionally toggle a Checkbox / Switch. | | [`dusk_snap`](tool-reference.md#dusk_snap) | Capture a YAML Semantics snapshot with `e` refs. | | [`dusk_tap`](tool-reference.md#dusk_tap) | Tap a widget by ref. | | [`dusk_triple_click`](tool-reference.md#dusk_triple_click) | Fire three primary clicks (~100ms apart). | | [`dusk_type`](tool-reference.md#dusk_type) | Type text into a TextField by ref. | | [`dusk_wait_for`](tool-reference.md#dusk_wait_for) | Wait until a UI condition is satisfied. | | [`dusk_wait_for_network_idle`](tool-reference.md#dusk_wait_for_network_idle) | Wait for zero in-flight HTTP requests. | Tool names are frozen: the `dusk_` shape is part of the alpha-2 cross-repo contract. Renames break agent prompts and pinned consumer scripts. --- ## Dispatch surfaces: `ext.dusk.*` vs. `artisan:dusk:*` The substrate MCP server inspects each descriptor's `extensionMethod` field to choose a dispatch path. Dusk uses two: - **`ext.dusk.*` (28 tools).** The default path. The MCP server calls `VmServiceClient.callServiceExtension(method, args)` against the running Flutter app's VM Service. The handler runs inside the app isolate and returns a `ServiceExtensionResponse`. This is the standard pattern; every action / inspection tool uses it. - **`artisan:dusk:*` (3 tools).** Dispatched in-process by the MCP server via the artisan registry, executing the matching CLI command (`dusk:hot_reload_and_snap`, `dusk:resize`, `dusk:device`). These three tools cannot run inside the app isolate: `dusk_hot_reload_and_snap` triggers `vm.reloadSources` against the very isolate that would be servicing the call (deadlock), and `dusk_resize_viewport` / `dusk_device_profile` drive Chrome DevTools Protocol on the host's Chromium subprocess. The substrate routes them through the CLI command instead so the orchestration runs outside the target isolate. Both paths return MCP `CallToolResult` content; the agent sees no difference at the JSON-RPC layer. The split exists purely so the same tool catalog can mix in-isolate VM Service calls and out-of-isolate CLI drivers without a second binary. --- ## Lifecycle: state file, lazy reconnect, snap-act loop Dusk inherits the substrate's lifecycle. The MCP server stays online even when no Flutter app is running: `~/.artisan/state.json` may be absent at `initialize` time, and the server still registers every tool descriptor. The first `tools/call` against a `dusk_*` tool triggers a lazy reconnect: the MCP server reads `state.json`, opens a WebSocket to the VM Service URI, and dispatches the call. Subsequent calls reuse the cached connection. The canonical agent loop: ``` dusk_snap -> capture Semantics tree, read refs dusk_find / read -> identify the target widget dusk_tap / type / -> drive an action against the ref scroll / drag dusk_wait_for / -> bridge async UI transitions wait_for_network_idle dusk_snap -> observe the new state, loop ``` The `e` refs from `dusk_snap` are frozen at snap time; re-snap after any navigation, modal open/close, or significant rebuild. `q` refs from `dusk_find` re-resolve on every action call so they survive rebuilds as long as the predicates still match. --- ## Related - [setup.md](setup.md): per-client install matrix (Claude Code, Cursor, Windsurf, VS Code, etc.) plus the reconnect ritual after editing `.mcp.json` or `.artisan/mcp.json`. - [tool-reference.md](tool-reference.md): per-tool input schema, return shape, and example JSON-RPC payload for every dusk tool. - [Substrate MCP overview](https://fluttersdk.com/artisan/mcp/overview): the underlying artisan MCP server that hosts the dusk tools. --- # Dusk MCP Setup # Dusk MCP Setup The dusk MCP tools ride on the substrate MCP server shipped inside [`fluttersdk_artisan`](https://fluttersdk.com/artisan/mcp/setup). Setup is therefore the substrate's `.mcp.json` plus a one-line provider registration: install the substrate, register `DuskArtisanProvider`, and the 31 `dusk_*` tools surface automatically. ## Prerequisites Before wiring any client config: 1. The consumer's `pubspec.yaml` depends on `fluttersdk_artisan` and `fluttersdk_dusk`. 2. The consumer's `bin/artisan.dart` (the standard scaffold from `dart run fluttersdk_dusk install`) lists `DuskArtisanProvider` in its provider factory list, or relies on the auto-discovered `lib/app/_plugins.g.dart` barrel populated by `dart run fluttersdk_dusk plugins:refresh`. 3. `dart run fluttersdk_dusk list` shows the dusk commands (`dusk:snap`, `dusk:tap`, etc.). When they are missing, the provider is not registered. The MCP server stays online with zero apps running. Plugin tool calls lazy-reconnect to the VM Service URI recorded in `~/.artisan/state.json` on first call. ## Automated install (recommended) `fluttersdk_artisan` ships a built-in MCP installer that writes the canonical `.mcp.json` entry for you. Once dusk is in `pubspec.yaml` and the provider is registered, the same single command wires Claude Code, Cursor, VS Code, Windsurf, and any other client that reads `.mcp.json` at the project root: ```bash dart run fluttersdk_dusk mcp:install ``` The installer is idempotent: pre-existing entries are preserved and the `fluttersdk` key is replaced in-place on re-run. Override the target path per client: ```bash # VS Code (per-project) dart run fluttersdk_dusk mcp:install --path .vscode/mcp.json # Cursor (per-project) dart run fluttersdk_dusk mcp:install --path .cursor/mcp.json ``` To remove the entry later: ```bash dart run fluttersdk_dusk mcp:uninstall ``` ## Canonical `.mcp.json` entry When writing the file by hand (or wiring a client whose path is not yet covered by `mcp:install --path`): ```json { "mcpServers": { "fluttersdk": { "command": "dart", "args": ["run", "fluttersdk_dusk", "mcp:serve"], "cwd": "." } } } ``` The `cwd` field must point at the project root (the directory that contains the consumer's `pubspec.yaml`). The server binary is the substrate's `bin/mcp.dart`; the dusk descriptors ride on it. ## Fallback invocations `mcp:install` picks the `.mcp.json` command/args payload based on the consumer's scaffold state. Three precedence levels, highest first: 1. **`./bin/fsa mcp:serve`** — when the fastcli scaffold is present and the platform is POSIX. Fastest startup (~50ms warm AOT). This is the default after `dart run fluttersdk_dusk dusk:install`. ```json { "command": "./bin/fsa", "args": ["mcp:serve"], "cwd": "." } ``` 2. **`dart run fluttersdk_dusk mcp:serve`** — when fastcli is absent (Windows or scaffold skipped). Auto-selected: the dusk wrapper injects `--invocation=fluttersdk_dusk` before forwarding `mcp:install` to the substrate, so the correct payload is written without any manual intervention. ~3s startup per call. This path surfaces all 31 dusk_* tools without scaffold dependency, because the wrapper forces `collectMcpTools: true` when dispatching `mcp:serve`. ```json { "command": "dart", "args": ["run", "fluttersdk_dusk", "mcp:serve"], "cwd": "." } ``` 3. **`dart run :dispatcher mcp:serve`** — legacy fallback when `mcp:install` is called directly through the substrate without a plugin invocation hint (e.g. a consumer that ran `dart run fluttersdk_dusk mcp:install` without going through the dusk wrapper). Prefer level 2 for dusk consumers; this form boots without dusk plugin tools unless the dispatcher is already configured. Trade-off: the fastcli path (level 1) requires the `./bin/fsa` AOT binary to be pre-compiled and present on the file system, but offers the fastest MCP server startup. The `dart run` path (level 2) works everywhere Dart is on PATH with no pre-compilation, at the cost of ~3s cold-start per agent session. ## Per-client install | Client | Config path | After edit | |---|---|---| | Claude Code | `.mcp.json` (project) or `~/.claude.json` (user) | `/mcp reconnect fluttersdk` | | Cursor | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (user) | Auto-reload | | VS Code (GitHub Copilot Workspace) | `.vscode/mcp.json` with `"servers"` key + `"type": "stdio"` | Reload window | | Continue | `.continue/config.json` `mcpServers` block | Restart Continue panel | | Windsurf | Windsurf MCP settings panel | Reload Cascade panel | | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows) | Fully quit + relaunch (close-window != quit) | For Claude Code via CLI: ```bash claude mcp add fluttersdk -- dart run fluttersdk_dusk mcp:serve # or project-scoped: claude mcp add --scope project fluttersdk -- dart run fluttersdk_dusk mcp:serve ``` For the VS Code MCP shape (note the `"servers"` key, not `"mcpServers"`, plus the explicit transport type): ```json { "servers": { "fluttersdk": { "type": "stdio", "command": "dart", "args": ["run", "fluttersdk_dusk", "mcp:serve"], "cwd": "." } } } ``` ## Reconnect ritual Claude Code, Continue, and Windsurf do NOT auto-reconnect when `.mcp.json` or `.artisan/mcp.json` (the visibility filter) changes. After every edit: - **Claude Code:** run `/mcp reconnect fluttersdk` in the chat. The slash command re-initializes only the named server; other MCP entries stay connected. - **VS Code / Continue:** reload the editor window or restart the panel. - **Windsurf:** reload the Cascade panel. - **Cursor:** picks up changes on its own; no manual action needed. The substrate MCP server stays online during a reconnect; the client simply re-runs `initialize` and reads the refreshed tool catalog. ## Troubleshooting **The server is online but no app is running.** This is normal. The substrate MCP server starts without requiring `~/.artisan/state.json`. Every `dusk_*` tool call returns an actionable error (`"VM Service unreachable: state.json missing"`) until you run `dart run fluttersdk_dusk start`. Once the state file exists the next tool call lazy-reconnects. **`dusk_*` tools missing from the catalog.** The provider is not registered. Verify by running `dart run fluttersdk_dusk list` and confirming the `dusk:*` command block. If the block is empty, add `DuskArtisanProvider.new` to the consumer's `artisanProviders` list in `bin/artisan.dart`, or run `dart run fluttersdk_dusk plugins:refresh` to regenerate the auto-discovery barrel. **Lazy-reconnect on first `tools/call` is slow (~1s).** Expected; the substrate opens a WebSocket to the VM Service URI on demand. Subsequent calls reuse the cached connection. **Tool visibility filter.** Hide a tool surface without uninstalling: edit `.artisan/mcp.json` to add the tool name under `tools.deny`. Deny always wins over allow. Run the reconnect ritual after editing. ## Related - [overview.md](overview.md): tool catalog, dispatch surfaces (`ext.dusk.*` vs. `artisan:dusk:*`), lifecycle. - [tool-reference.md](tool-reference.md): per-tool input schema and example payloads. - [Substrate MCP setup](https://fluttersdk.com/artisan/mcp/setup): the full per-client install matrix for the underlying server. --- # Dusk MCP Tool Reference # Dusk MCP Tool Reference Per-tool input schema, return shape, and example payload for every `dusk_*` MCP tool contributed by `DuskArtisanProvider`. 31 tools total: 28 dispatch through `ext.dusk.*` VM Service extensions and 3 (`dusk_hot_reload_and_snap`, `dusk_resize_viewport`, `dusk_device_profile`) route through the `artisan:dusk:*` substrate path to a CLI command because the orchestration cannot run inside the target isolate. Sections are ordered alphabetically. Every section names the dispatch surface (`extensionMethod`) at the top so the consumer knows which path the server takes. All example payloads show the `params.arguments` object inside the `tools/call` JSON-RPC request; the substrate MCP server wraps the response as `CallToolResult` text content. ## Table of contents - [`dusk_blur`](#dusk_blur) - [`dusk_clear`](#dusk_clear) - [`dusk_close_app`](#dusk_close_app) - [`dusk_console`](#dusk_console) - [`dusk_dblclick`](#dusk_dblclick) - [`dusk_device_profile`](#dusk_device_profile) - [`dusk_dismiss_modals`](#dusk_dismiss_modals) - [`dusk_drag`](#dusk_drag) - [`dusk_evaluate`](#dusk_evaluate) - [`dusk_exceptions`](#dusk_exceptions) - [`dusk_find`](#dusk_find) - [`dusk_focus`](#dusk_focus) - [`dusk_get_routes`](#dusk_get_routes) - [`dusk_hot_reload_and_snap`](#dusk_hot_reload_and_snap) - [`dusk_hover`](#dusk_hover) - [`dusk_navigate`](#dusk_navigate) - [`dusk_navigate_back`](#dusk_navigate_back) - [`dusk_observe`](#dusk_observe) - [`dusk_press_key`](#dusk_press_key) - [`dusk_resize_viewport`](#dusk_resize_viewport) - [`dusk_right_click`](#dusk_right_click) - [`dusk_screenshot`](#dusk_screenshot) - [`dusk_scroll`](#dusk_scroll) - [`dusk_select_option`](#dusk_select_option) - [`dusk_set_checkbox`](#dusk_set_checkbox) - [`dusk_snap`](#dusk_snap) - [`dusk_tap`](#dusk_tap) - [`dusk_triple_click`](#dusk_triple_click) - [`dusk_type`](#dusk_type) - [`dusk_wait_for`](#dusk_wait_for) - [`dusk_wait_for_network_idle`](#dusk_wait_for_network_idle) --- ## dusk_blur Dispatch: `ext.dusk.blur` Clear keyboard focus from whatever currently holds it (Playwright `locator.blur()` / `document.activeElement.blur()` parity). ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `includeSnapshot` | boolean | no | Embed the post-blur snapshot in the response. Default `false`. | ### Returns Success: `{ blurred: true, hadFocus: bool }`. `hadFocus` is `false` when no node held focus at call time (still treated as success, idempotent). Error: returned via MCP `isError: true` when the focus-tree walk fails internally. ### Example call ```json { "name": "dusk_blur", "arguments": { "includeSnapshot": false } } ``` Response: ```json { "blurred": true, "hadFocus": true } ``` --- ## dusk_clear Dispatch: `ext.dusk.clear` Empty the `TextEditingController` backing the resolved text field (Playwright `locator.clear()` parity). ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `ref` | string | yes | Widget ref of a `TextField` / `TextFormField` / `EditableText` (e.g. `"e5"`). | | `includeSnapshot` | boolean | no | Embed the post-clear snapshot. Default `false`. | ### Returns Success: `{ ref: "e", text: "" }`. Error: `DuskActionabilityException` (when the gate fails) or `DuskStaleHandleException` (when the ref is unknown / stale) surfaced as the wire error string `"Widget ref= is not actionable: "`. ### Example call ```json { "name": "dusk_clear", "arguments": { "ref": "e5" } } ``` Response: ```json { "ref": "e5", "text": "" } ``` --- ## dusk_close_app Dispatch: `ext.dusk.close_app` Request a graceful shutdown of the running Flutter app via `SystemNavigator.pop()`. On mobile + desktop this terminates the app; on web the call is a no-op (browsers do not allow programmatic tab close). ### Input schema No parameters. ### Returns Success: an empty object `{}`. After the call the VM Service URI is gone, so the next `dusk_*` tool returns a VM-Service-unreachable error. ### Example call ```json { "name": "dusk_close_app", "arguments": {} } ``` --- ## dusk_console Dispatch: `ext.dusk.console` Read recent log entries from the running app's telescope store. Missing-telescope graceful: returns an empty list when the host has not wired telescope. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | Maximum number of entries to return. Default `50`. | | `minLevel` | string | no | Minimum severity level (`INFO`, `WARNING`, `ERROR`). Omit for all levels. | ### Returns Success: `{ entries: [ { level, message, time, logger }, ... ] }`. Error: never; missing telescope is treated as the empty-entries success path. ### Example call ```json { "name": "dusk_console", "arguments": { "limit": 10, "minLevel": "ERROR" } } ``` --- ## dusk_dblclick Dispatch: `ext.dusk.dblclick` Double-click a widget by ref. Synthesizes two pointer Down+50ms+Up sequences at the widget's center with ~100ms between them (Playwright double-click model). Triggers `GestureDetector.onDoubleTap`. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `ref` | string | yes | Widget ref from a prior `dusk_snap`. Shape `e` or `q`. | ### Returns Success: `{ ref: "e" }`. The actionability gate runs once before the first tap; the post-action snapshot is captured after the second tap completes. Error: `"Widget ref= is not actionable: "` (gate failure) or stale-handle error when the ref is unknown. ### Example call ```json { "name": "dusk_dblclick", "arguments": { "ref": "e7" } } ``` --- ## dusk_device_profile Dispatch: `artisan:dusk:device` Emulate a named device profile (viewport + DPR + touch + user agent) via Chrome DevTools Protocol. Requires the substrate to have been started with `--cdp-port`. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `preset` | string | no | One of `iphone-x`, `iphone-13`, `iphone-15-pro`, `pixel-5`, `pixel-8`, `ipad-pro-12.9`, `desktop-1440`, `desktop-1920`. Omit when using `list` or `reset`. | | `list` | boolean | no | List all available presets. When `true`, `preset` + `reset` are ignored. Default `false`. | | `reset` | boolean | no | Clear all viewport overrides (metrics + touch + user agent). Default `false`. | ### Returns Success (`preset`): `{ applied: "", viewport: { width, height, dpr, mobile, touch } }`. Success (`list`): `{ presets: [ { name, width, height, dpr, mobile }, ... ] }`. Success (`reset`): `{ reset: true }`. Error: unknown preset name (the response suggests running with `list: true`); `cdpPort` not configured. ### Example call ```json { "name": "dusk_device_profile", "arguments": { "preset": "iphone-x" } } ``` --- ## dusk_dismiss_modals Dispatch: `ext.dusk.dismiss_modals` Pop every modal route (dialog, bottom sheet, popup) currently above the first persistent route. Idempotent. ### Input schema No parameters. ### Returns Success: `{ popped: }`: the number of modals that were popped. `0` when no modals were open. Error: never; safe to call speculatively. ### Example call ```json { "name": "dusk_dismiss_modals", "arguments": {} } ``` --- ## dusk_drag Dispatch: `ext.dusk.drag` Drag from one widget to another by ref tokens. Synthesizes pointer Down + 5x intermediate Move events + Up sequence from `startRef`'s center to `endRef`'s center. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `startRef` | string | yes | Source widget ref (`e`). | | `endRef` | string | yes | Target widget ref (`e`). | ### Returns Success: `{ startRef, endRef }`. Both refs are echoed for caller bookkeeping. Error: actionability gate failure on either ref, or stale-handle on either. ### Example call ```json { "name": "dusk_drag", "arguments": { "startRef": "e12", "endRef": "e18" } } ``` --- ## dusk_evaluate Dispatch: `ext.dusk.evaluate` Evaluate a Dart expression in the running app isolate via the Tinker bridge (`ext.tinker.evaluate`). MCP-only: `magic_tinker` owns the connected REPL surface. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `expression` | string | yes | Single Dart expression. No statements, no trailing semicolon. | ### Returns Success: `{ result: "" }`. Error: returns an MCP error when the Tinker plugin is not installed; never crashes the app. ### Example call ```json { "name": "dusk_evaluate", "arguments": { "expression": "Auth.user?.email" } } ``` Response: ```json { "result": "user@example.com" } ``` --- ## dusk_exceptions Dispatch: `ext.dusk.exceptions` Read recent exception entries from the telescope exception watcher. Missing-telescope graceful: returns an empty list when telescope is not wired. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | Maximum number of entries to return. Default `20`. | ### Returns Success: `{ entries: [ { type, message, stackHead, time }, ... ] }`. `stackHead` is the first 3 lines of the stack trace. ### Example call ```json { "name": "dusk_exceptions", "arguments": { "limit": 5 } } ``` --- ## dusk_find Dispatch: `ext.dusk.find` Find a widget by semantic query (text / semanticsLabel / key) and return a re-resolvable `q` handle. Unlike snapshot-frozen `e` refs, `q` re-executes the tree walk on every subsequent action call, so the handle survives widget rebuilds as long as the predicates still match. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `text` | string | no | Exact match against accessibility label first, then `Text.data`. | | `contains` | string | no | Substring match against accessibility label first, then `Text.data` (case-sensitive). Use when the visible label is dynamic (counters, timestamps, plurals). | | `semanticsLabel` | string | no | Exact match against `SemanticsNode.label` only (no Text fallback). | | `key` | string | no | Match against a widget `Key`. For `ValueKey`, pass the inner value's `toString()`. | At least one of the four must be supplied. When multiple are passed they form an intersection. ### Returns Success on first match: `{ ref: "q", matched: true }`. No match: `{ ref: null, matched: false }`. Error: surfaced only when a follow-up action call finds zero live matches against the handle (`"stale handle"`); the agent must re-find or re-snap, never silently retry. ### Example call ```json { "name": "dusk_find", "arguments": { "text": "Submit" } } ``` --- ## dusk_focus Dispatch: `ext.dusk.focus` Request keyboard focus on the widget identified by `ref` (Playwright `locator.focus()` parity). Walks to the nearest `Focus` ancestor and calls `requestFocus()`. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `ref` | string | yes | Widget ref (e.g. `"e5"`). | | `includeSnapshot` | boolean | no | Embed the post-focus snapshot. Default `false`. | ### Returns Success: `{ ref: "", focused: true }`. ### Example call ```json { "name": "dusk_focus", "arguments": { "ref": "e5" } } ``` --- ## dusk_get_routes Dispatch: `ext.dusk.get_routes` List the route paths declared by the running app's `MagicRouter`. Returns an empty list when no Magic router is installed. ### Input schema No parameters. ### Returns Success: `{ routes: [ { path, name }, ... ] }`. Parameterised paths render with `:id`-style placeholders. ### Example call ```json { "name": "dusk_get_routes", "arguments": {} } ``` Response: ```json { "routes": [ { "path": "/monitors", "name": "monitors.index" }, { "path": "/monitors/:id", "name": "monitors.show" } ] } ``` --- ## dusk_hot_reload_and_snap Dispatch: `artisan:dusk:hot_reload_and_snap` Hot reload the running Flutter app, then capture a snapshot, screenshot, and recent exceptions in one round-trip. Routes through the CLI command because an in-isolate handler cannot reload itself. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `screenshot` | boolean | no | Capture a screenshot after the reload. Default `true`. | ### Returns Success: `{ reloaded: true, durationMs: , snapshot: "", screenshot: "", recentExceptions: [...] }`. Compile error: `{ reloaded: false, durationMs: , error: "", recentExceptions: [...] }`. `snapshot` + `screenshot` are omitted on compile error. ### Example call ```json { "name": "dusk_hot_reload_and_snap", "arguments": { "screenshot": false } } ``` --- ## dusk_hover Dispatch: `ext.dusk.hover` Hover a mouse cursor over a widget by ref. Mouse-only (no touch equivalent). Synthesizes a `PointerHoverEvent` of `PointerDeviceKind.mouse` at the widget's center. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `ref` | string | yes | Widget ref (`e`). | ### Returns Success: `{ ref: "" }`. No-op on touch-only devices. Error: actionability gate failure, or stale-handle. ### Example call ```json { "name": "dusk_hover", "arguments": { "ref": "e8" } } ``` --- ## dusk_navigate Dispatch: `ext.dusk.navigate` Navigate the running Flutter app to a route path. Resolves through `MagicRoute.to(...)` when Magic is installed, falling back to `Navigator.of(root).pushNamed(...)`. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `route` | string | yes | Route path. Must start with `/`. Example: `/monitors/123`. | ### Returns Success: `{ route: "" }`. ALWAYS re-snap after; refs from a prior snapshot are invalidated. ### Example call ```json { "name": "dusk_navigate", "arguments": { "route": "/login" } } ``` --- ## dusk_navigate_back Dispatch: `ext.dusk.navigate_back` Pop the top route off the active navigator stack. Equivalent to pressing the system Back button. No-op when the stack has only one route. ### Input schema No parameters. ### Returns Success: `{ popped: bool }`. `false` when the stack already had only one route. ### Example call ```json { "name": "dusk_navigate_back", "arguments": {} } ``` --- ## dusk_observe Dispatch: `ext.dusk.observe` Return a structured candidate list of every interactive widget on screen. Implements Stagehand's observe-once-act-many pattern (no server-side LLM). Each candidate carries a re-resolvable `q` ref. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `intent` | string | no | Free-form caller hint (e.g. `"login form"`). Echoed in audit logs, NOT used server-side. | | `roles` | string | no | Comma-separated role filter (`button,textbox,link,checkbox,heading,image`). Omit for every role. | | `limit` | integer | no | Maximum number of candidates. Default `50`. | | `includeEnrichers` | string | no | `"true"` (default subset), `"false"` (none), or `"full"` (every field). | ### Returns Success: `{ candidates: [ { ref, role, label, value, bounds, isEnabled, isVisible, enrichers: { ... } }, ... ] }`. The enricher subset projects `magicFormField`, `magicRoute`, `magicGateResult`, `wind.breakpoint`, `wind.states` by default. ### Example call ```json { "name": "dusk_observe", "arguments": { "intent": "login form", "roles": "textbox,button", "limit": 20 } } ``` --- ## dusk_press_key Dispatch: `ext.dusk.press_key` Press a hardware key (optionally with modifiers). Synthesizes `KeyDownEvent` + `KeyUpEvent` through `ServicesBinding.instance.keyboard.handleKeyEvent`. ### Input schema | Parameter | Type | Required | Description | |---|---|---|---| | `key` | string | yes | Logical key label (e.g. `Enter`, `Escape`, `Tab`, `ArrowDown`, `S`). | | `modifiers` | array | no | Subset of `control`, `shift`, `alt`, `meta` held during the press. | ### Returns Success: `{ key: "