with one click
flutter-widget-tree
// Analyze the Flutter widget tree from the Dart Tooling Daemon. Extracts panel layout, item ordering, block IDs, and widget hierarchy for debugging UI issues.
// Analyze the Flutter widget tree from the Dart Tooling Daemon. Extracts panel layout, item ordering, block IDs, and widget hierarchy for debugging UI issues.
| name | flutter-widget-tree |
| description | Analyze the Flutter widget tree from the Dart Tooling Daemon. Extracts panel layout, item ordering, block IDs, and widget hierarchy for debugging UI issues. |
The Dart Tooling Daemon must be connected first:
mcp__dart__connect_dart_tooling_daemon(uri: "<ws://...>")
Always find the DTD URI from the live process — never trust a stale log file.
# 1. Find the flutter run process
ps aux | grep 'flutter_tools.snapshot run' | grep -v grep
# Note the PID (e.g. 19772)
# 2. Find the sibling `tee` process under the same parent shell
ps -eo pid,ppid,command | grep "$(ps -o ppid= -p <flutter_pid> | tr -d ' ')" | grep tee
# Output: 19773 36667 /usr/bin/tee /tmp/flutter.log
# The tee target is the log file for THIS run
# 3. Extract the DTD URI from that log file
head -25 /tmp/flutter.log | grep 'Dart Tooling Daemon'
# Output: The Dart Tooling Daemon is available at: ws://127.0.0.1:<port>/<token>
mcp__dart__get_widget_tree(summaryOnly: true)
The result is too large for inline display. It gets saved to a file under
.claude/projects/.../tool-results/mcp-dart-get_widget_tree-*.txt.
The file contains a JSON array with one element:
[{"type": "text", "text": "{\"description\":\"[root]\", ...}"}]
The inner text field is an escaped JSON string containing the actual widget tree.
Each node has:
description: Widget identity string (e.g. "TreeNodeWidget-[<'block:uuid'>]")widgetRuntimeType: Class name (e.g. "TreeNodeWidget", "BlockRefWidget")createdByLocalProject: true for user code, absent/false for framework widgetschildren: Array of child nodesvalueId: Inspector identifier (e.g. "inspector-42")cat <file> | jq '.[0].text | fromjson'
cat <file> | jq '.[0].text | fromjson | [.. | .widgetRuntimeType? // empty] | unique | sort[]'
cat <file> | jq '.[0].text | fromjson | [.. | select(.createdByLocalProject? == true) | .widgetRuntimeType] | group_by(.) | map({type: .[0], count: length}) | sort_by(-.count)[]'
cat <file> | jq '.[0].text | fromjson | .. | select(.widgetRuntimeType? == "TreeNodeWidget") | .description'
cat <file> | jq -r '.[0].text | fromjson | .. | select(.widgetRuntimeType? == "TreeNodeWidget") | .description | capture("block:(?<id>[a-f0-9-]+)") | .id'
The widget tree follows this hierarchy:
RootWidget → ProviderScope → MyApp → PlatformMenuBar → WindowBorder → Shortcuts → Actions → MaterialApp → MainScreen → Scaffold
└─ Column
├─ WindowTitleBarBox
└─ Expanded
└─ ReactiveQueryWidget (root layout)
├─ BlockRefWidget (right sidebar)
│ └─ ReactiveQueryWidget → ListItemWidgets
└─ Row (main content area)
├─ Flexible (left sidebar)
│ └─ BlockRefWidget → ReactiveQueryWidget → ListItemWidgets (documents)
├─ Flexible (main panel)
│ └─ BlockRefWidget → ReactiveQueryWidget → TreeViewWidget → TreeNodeWidgets
└─ (optional more Flexibles)
.children[0].children[0].children[0].children[0] # ProviderScope → MyApp
.children[0].children[0].children[0].children[0] # PlatformMenuBar → Scaffold
.children[0].children[0].children[0].children[0].children[0] # Column
Shortcut: 11 levels of .children[0] from root to Scaffold.
This shows the structural layout with user-created widgets only:
cat <file> | jq -r '
.[0].text | fromjson
| .children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0]
| .children[0].children[1]
| def walk_user(depth):
select(.createdByLocalProject? == true) |
(if .widgetRuntimeType == "Row" or .widgetRuntimeType == "Column" or .widgetRuntimeType == "Expanded" or .widgetRuntimeType == "Flexible" or .widgetRuntimeType == "BlockRefWidget" or .widgetRuntimeType == "ReactiveQueryWidget" or .widgetRuntimeType == "TreeViewWidget" or .widgetRuntimeType == "TreeViewWidgetContent" or .widgetRuntimeType == "ListItemWidget" or .widgetRuntimeType == "TreeNodeWidget" or .widgetRuntimeType == "SourceEditorWidget" or .widgetRuntimeType == "SearchSelectOverlay" or .widgetRuntimeType == "WildcardOperationsWidget" then
(" " * depth + .description),
(.children[]? | walk_user(depth + 1))
else
(.children[]? | walk_user(depth))
end);
walk_user(0)
'
The Main Panel is a TreeViewWidget inside the second Flexible of the Row:
cat <file> | jq -r '.[0].text | fromjson | .. | select(.widgetRuntimeType? == "TreeViewWidgetContent") | [.. | select(.widgetRuntimeType? == "TreeNodeWidget") | .description | capture("block:(?<id>[a-f0-9-]+)") | .id] | .[]'
cat <file> | jq -r '.[0].text | fromjson | .. | select(.widgetRuntimeType? == "ListItemWidget") | .description | capture("doc:(?<id>[a-f0-9-]+)") | .id'
To check whether blocks are displayed in the same order as in an org file:
grep -oP 'ID: block:\K[a-f0-9-]+' file.org# UI order
cat <file> | jq -r '...(as above)...' > /tmp/ui_order.txt
# Org file order
grep -oP 'ID: block:\K[a-f0-9-]+' /path/to/file.org > /tmp/org_order.txt
# Diff
diff /tmp/org_order.txt /tmp/ui_order.txt
| Widget | Role |
|---|---|
ReactiveQueryWidget | Live query container, subscribes to CDC changes |
BlockRefWidget | Renders a block by ID via FFI renderBlock() |
TreeViewWidget / TreeViewWidgetContent | Animated tree (main panel outline) |
TreeNodeWidget | Single tree node, key contains block:<uuid> |
ListItemWidget | Flat list item, key contains block:<uuid> or doc:<uuid> |
EditableTextField | Inline text editor for block content |
SourceEditorWidget | Code/query source block editor |
SearchSelectOverlay | Search/filter popup |
WildcardOperationsWidget | Operations toolbar |
[HINT] Download the complete skill directory including SKILL.md and all related files