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.
Interactive nodes inside a currently-overflowing render ancestor carry an additive overflow: true sub-line in the YAML output. This is a live current-state check (RenderFlex.toStringShort() appends OVERFLOWING while the constraint is violated); it is not a historical record. For the full non-fatal error history including overflow details, use dusk:exceptions.
Text fields collapse to a single typeable ref. A field such as a wind WInput produces two nested textbox Semantics nodes (the outer wrapper plus the RenderEditable leaf that always owns its own node). dusk:snap suppresses the inner duplicate when its render object is a render-tree descendant of the outer textbox node's render object, emitting one ref for the surviving node and tagging it with an additive typeable: true sub-line. Collapse is by render-object containment only, so two sibling fields sharing the same label stay two distinct refs. Target the node carrying typeable: true when typing: dusk:type resolves the inner EditableText from it.
Table of contents
Synopsis
dart run fluttersdk_dusk dusk:snap [--depth=]
[--within=]
[--interactiveOnly]
[--grep=]
[--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. |
--within |
string (e) |
unset (whole tree) | Scope the walk to one subtree. Takes an e ref from a prior snapshot; q query handles are not addressable here, since they re-resolve per action and have no fixed subtree. An unknown or node-less ref is an error rather than a silent widening. |
--interactiveOnly |
flag | false |
Emit only nodes that carry a ref (buttons, text fields, links, headers, anything with a tap action) and drop the plain - text lines. |
--grep |
string (regex) | unset | Emit only nodes whose label or value matches, plus the ancestors that lead to them. Ancestors survive because they carry the refs you act on: a matching - text line has none of its own. |
Every flag is serialised as a string into the VM Service params map; the command does no key translation.
Narrowing, and why the full tree is usually the wrong answer
A full tree costs context on any real screen, and on a shell whose sidebar repeats the labels of the pages it opens it is also the misleading answer: an exact-label lookup resolves the nav item, so the caller measures the sidebar and concludes two pages differ. That has produced confident wrong readings more than once.
# Read one region. Take its ref from a prior snapshot.
dart run fluttersdk_dusk dusk:snap --within=e12
# Only the things you can act on.
dart run fluttersdk_dusk dusk:snap --interactiveOnly
# Only what matches, plus the path to it.
dart run fluttersdk_dusk dusk:snap --grep="Sign ?in"
The three compose. --within=e12 --interactiveOnly is the usual shape before an action: one region, refs only.
An unfiltered call is unchanged, byte for byte.
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):
[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]
typeable: true
[ref=e3] role=text label="Welcome back" rect=(20,80,335,32)
The typeable: true sub-line marks the single surviving textbox node after the nested-field collapse described above; it is the node dusk:type resolves.
When --includeEnrichers is set, each entry gains indented lines contributed by the registered enrichers (see Enricher fragments).
Render errors
When the FlutterError.onError capture buffer holds non-fatal render or build errors (for example a ParentDataWidget misuse such as Expanded placed outside a Flex, or a layout overflow), the response includes a renderErrors block:
{
"snapshot": "",
"groupId": "snapshot-1700000000000",
"renderErrors": {
"count": 2,
"recent": [
{ "type": "FlutterError", "message": "Incorrect use of ParentDataWidget." },
{ "type": "FlutterError", "message": "A RenderFlex overflowed by 32 pixels." }
],
"hint": "Run dusk:exceptions (CLI) or dusk_exceptions (MCP) for full messages + stack traces."
}
}
Fields:
| Field | Type | Description |
|---|---|---|
count |
int | Total number of captured render/build errors. May exceed recent.length. |
recent |
array (max 3) | Most recent entries; each has type (string) and message (first line only). |
hint |
string | Fixed advisory pointing to dusk:exceptions (CLI) / dusk_exceptions (MCP) for full detail. |
The renderErrors block is omitted entirely when no errors are in the buffer. A clean screen produces the standard two-key envelope (snapshot + groupId).
CLI banner: when renderErrors is present, dusk:snap prints a render-error banner to stderr (stdout stays the pure snapshot text, so tooling that captures only stdout is unaffected):
⚠ 2 render error(s) captured on this screen (run dusk:exceptions for full detail):
- FlutterError: Incorrect use of ParentDataWidget.
- FlutterError: A RenderFlex overflowed by 32 pixels.
Frame production
When the app is not producing frames, semantics labels are never rebuilt: the snapshot comes back with its buttons and none of its - text nodes, and a screen that renders perfectly reads as completely empty. The response then carries a warnings block and dusk:snap prints a banner to stderr:
⚠ The app is not producing frames (lifecycle: hidden). The semantics tree is not being
rebuilt and gestures cannot take effect, so this result may be stale. A backgrounded
browser tab is the usual cause: bring the page to front and retry.
Do not read an empty tree as an empty screen without checking for this block first. See Frame production.
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:snapruns). DuskPlugin.install()not wired inlib/main.dart(rundusk: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 neutralfluttersdk_wind_diagnostics_contracts.WindDebugRegistrybridge 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.
Overflow annotation
The overflow: true sub-line is emitted by the snapshot dispatcher itself (not via an enricher) when the resolved RenderObject.toStringShort() contains the Flutter debug suffix OVERFLOWING. It appears for any interactive node whose nearest render ancestor is currently violating its constraints, regardless of whether enrichers are enabled. Example output:
[ref=e5] role=button label="Submit" rect=(0,0,320,48) actions=[tap]
overflow: true
Typeable annotation and textbox collapse
A text field commonly produces two nested textbox Semantics nodes: an outer wrapper (e.g. a wind WInput's Semantics(textField:true)) and the RenderEditable leaf, which always owns its own textField node and which MergeSemantics cannot absorb. Both nodes used to mint a ref; typing on the inner leaf threw -32000. The snapshot walk now suppresses the inner node when its render object is a render-tree descendant of the outer textbox node's render object, leaving one ref for the surviving node and appending typeable: true beneath it:
[ref=e2] role=textbox label="Email" rect=(20,200,335,56) actions=[tap, focus, type]
typeable: true
The collapse is by render-object containment only, never label/value equality, so two sibling fields sharing a label remain two distinct refs. The typeable: true node is the one dusk:type resolves to the inner EditableText.
Examples
1. Minimal snapshot of the current screen
dart run fluttersdk_dusk dusk:snap
Expected output (illustrative; truncated):
[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
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
dart run fluttersdk_dusk dusk:snap --includeEnrichers
Expected output (illustrative):
[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: consume an
eNtoken to synthesise a tap. - dusk:find: mint a long-lived
qNhandle that survives rebuilds. - dusk:observe: structured candidate list when the agent needs more than the raw Semantics tree.
- Plugins: Enricher authoring: how to ship a custom enricher that contributes additional fragments.