
Maui Ai Debugging
- 1 installs
- 28 repo stars
- Updated July 18, 2026
- davidortinau/sentencestudio
Build, deploy, inspect, and debug .NET MAUI and Blazor Hybrid apps from the terminal, driving the running app's UI, logs, network, and storage via the maui devflow CLL.
About
Provides an end-to-end build-deploy-inspect-fix loop for .NET MAUI apps across iOS, Android, Mac Catalyst, macOS, and Linux using the maui devflow tool. A developer uses it to inspect the visual tree, tap and fill UI, read logs and network traffic, and manage simulators as an AI agent.
- Inspect and interact with running MAUI UI via ui tree, tap, fill, and screenshot commands
- Covers simulators, Blazor WebView CDP, network monitoring, and app storage inspection
Maui Ai Debugging by the numbers
- 1 all-time installs (skills.sh)
- Ranked #489 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/sentencestudio --skill maui-ai-debuggingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 28 |
| Last updated | July 18, 2026 |
| Repository | davidortinau/sentencestudio ↗ |
What it does
Build, deploy, inspect, and debug .NET MAUI and Blazor Hybrid apps from the terminal, driving the running app's UI, logs, network, and storage via the maui devflow CLL.
Files
MAUI AI Debugging
Build, deploy, inspect, and debug .NET MAUI apps from the terminal. This skill enables a complete feedback loop: build → deploy → inspect → fix → rebuild.
Prerequisites
dotnet tool install --global Microsoft.Maui.Cli || dotnet tool update --global Microsoft.Maui.Cli
dotnet tool install --global androidsdk.tool # Android only
dotnet tool install --global appledev.tools # iOS/Mac onlyVerify Install
Confirm the tool is installed and functioning:
maui devflow --versionKeep the skill up to date: maui devflow update-skill. Check installed version vs remote with maui devflow skill-version. For full update procedures, see references/setup.md.
Integrating MauiDevFlow into a MAUI App
For complete setup instructions, see references/setup.md.
Quick summary: 1. Add NuGet packages (Microsoft.Maui.DevFlow.Agent, and Microsoft.Maui.DevFlow.Blazor for Blazor Hybrid)
- For Linux/GTK apps (detected via
grep -i 'GirCore\|Maui\.Gtk' *.csproj), useAgent.GtkandBlazor.Gtkinstead - For macOS (AppKit) apps (detected via
grep -i 'Platform\.Maui\.MacOS' *.csproj), the standardAgentandBlazorpackages include macOS support
2. Register in MauiProgram.cs inside #if DEBUG 3. For Blazor Hybrid: chobitsu.js is auto-injected (no manual script tag needed) 4. For Mac Catalyst: ensure network.server entitlement 5. For Android: run adb reverse for broker + agent ports 6. For Linux: no special network setup needed (direct localhost) 7. For macOS (AppKit): separate app head project, uses open App.app to launch. See references/macos.md
Core Workflow
0. Verify DevFlow Availability
Before building or launching anything, determine if DevFlow is available for the current project.
Check integration (project files — the source of truth):
# Check if any csproj in the project has DevFlow packages
grep -rl "MauiDevFlow\|Maui\.DevFlow" --include="*.csproj" .If grep returns results, DevFlow IS integrated — even if maui devflow list shows nothing.
Check runtime connection:
maui devflow list # shows connected agents
maui devflow broker status # shows broker health
maui devflow diagnose # full end-to-end health check (recommended)Decision tree — what to do based on results:
| Project has DevFlow packages? | Agent in list? | Action |
|---|---|---|
| ✅ Yes | ✅ Yes | Ready — proceed to inspection/interaction |
| ✅ Yes | ❌ No | App not running in Debug, or broker issue. Launch app, then maui devflow wait |
| ❌ No | — | Need to integrate DevFlow (see "Integrating MauiDevFlow into a MAUI App") |
⚠️ CRITICAL: maui devflow list shows RUNTIME state (connected agents), NOT project integration. An empty list does NOT mean "DevFlow is not installed." Always check project files first.
After launching the app (via `dotnet build -t:Run` or through Aspire):
maui devflow wait # blocks until agent connects (default 120s)
maui devflow wait --project path/to/App.csproj # filter to specific projectALWAYS run wait after launching. Never assume the agent is connected — verify it.
1. Ensure a Device/Simulator/Emulator is Running
TFM-to-Minimum-Simulator-Runtime Mapping
| TFM | Minimum iOS Sim | Minimum Android API |
|---|---|---|
| net10.0-ios | iOS 26.0 | — |
| net9.0-ios | iOS 17.0 | — |
| net10.0-android | — | API 24 |
| net9.0-android | — | API 24 |
CRITICAL: ALWAYS check the project TFM BEFORE selecting a simulator. An older simulator may have a stale app install that appears to "work" but runs an incompatible binary with a broken database. NEVER trust a pre-existing app install — always do a fresh build + deploy.
# Example: project targets net10.0-ios → need iOS 26+ simulator
grep -i 'TargetFrameworks' *.csproj | grep -o 'net[0-9]*\.[0-9]-ios'
xcrun simctl list devices available | grep "iOS 26"⚠️ Multi-project conflict avoidance: When multiple projects may run simultaneously (common with AI agents), each project should use its own dedicated simulator/emulator to prevent apps from replacing each other. Check what's already in use first:
maui devflow list # check if any agents are already connected (runtime state only — see Step 0 for integration check)If another iOS or Android agent is already registered, create a new simulator/emulator for your project instead of reusing the one that's already booted.
iOS Simulator:
xcrun simctl list devices booted # check booted sims
# Create a project-dedicated simulator to avoid conflicts
xcrun simctl create "MyApp-iPhone17Pro" "iPhone 17 Pro" "iOS 26.2"
xcrun simctl boot <UDID> # boot the new simAndroid Emulator:
android avd list # list AVDs
# Create a project-dedicated emulator to avoid conflicts
android avd create --name "MyApp-Pixel8" \
--sdk "system-images;android-35;google_apis;arm64-v8a" --device pixel_8
android avd start --name "MyApp-Pixel8"Mac Catalyst / macOS (AppKit) / Linux/GTK: No device setup needed — runs as desktop app. Multiple desktop apps can run simultaneously without conflicts.
Simulator & Emulator State Tracking
After every successful DevFlow connection, record the simulator/emulator details to .claude/skills/maui-ai-debugging/references/device-state.json:
{
"lastSuccessful": {
"platform": "iOS",
"deviceName": "iPhone 16 Pro",
"udid": "802B6FB8-...",
"runtime": "iOS 26.2",
"tfm": "net10.0-ios",
"appBundleId": "com.simplyprofound.sentencestudio",
"hasData": true,
"lastUsed": "2026-04-12T22:00:00Z",
"outcome": "success"
},
"lastFailed": null
}Rules for device selection:
- On session start: Read
device-state.jsonto pick up where the last session left off. - Task requires existing data: Prefer the
lastSuccessfuldevice — it has a working
app install with real data. Boot it and reconnect.
- Task requires a CLEAN start: Use a DIFFERENT simulator. Create a new one if needed
(e.g., xcrun simctl create "CleanTest-iPhone16" "iPhone 16 Pro" "iOS 26.2"). Do NOT reuse the lastSuccessful device and risk destroying its data.
- Always update this file after testing — record both successes and failures.
- After a failure: Record details in
lastFailedso the next session can avoid the
same device/configuration.
2. Detect the TFM
IMPORTANT: Before building, detect the correct Target Framework Moniker from the project. Do NOT assume net10.0 — many projects use net9.0, net8.0, etc.
grep -i 'TargetFrameworks' *.csproj Directory.Build.props 2>/dev/nullUse the detected version (e.g. net9.0) in all build commands. The examples use $TFM.
3. Build, Deploy, and Connect
Follow these steps for every launch and rebuild.
Step 1: Kill any previous instance (skip on first launch). A stale app's agent stays registered with the broker, causing maui devflow wait to return the old port instantly instead of waiting for the new build.
# Stop the async shell from the previous launch, then confirm:
maui devflow list # should show no agents (or only unrelated ones)Step 2: Launch in an async shell.
# iOS Simulator
dotnet build -f $TFM-ios -t:Run -p:_DeviceName=:v2:udid=<UDID>
# Android Emulator
dotnet build -f $TFM-android -t:Run
# Mac Catalyst
dotnet build -f $TFM-maccatalyst -t:Run
# macOS AppKit — build exits after compiling; launch separately
dotnet build -f $TFM-macos <path-to-macos-project>
open path/to/bin/Debug/$TFM-macos/osx-arm64/AppName.app
# Linux/GTK
dotnet run --project <path-to-gtk-project>⚠️ Process lifecycle rules:
dotnet build -t:Run(iOS, Android, Mac Catalyst) anddotnet run(Linux/GTK) **block
for the lifetime of the app. Killing or stopping the shell kills the app**. Use mode: "async" with initial_wait: 120 and do NOT stop the shell until you are done.
- macOS (AppKit) is the exception:
dotnet buildexits after compiling, andopen
launches the app independently — the app survives shell termination.
Step 3: Wait for the agent — never use sleep.
maui devflow wait # blocks until agent registers (default 120s)
maui devflow wait --project path/to/App.csproj # filter to specific projectmaui devflow wait prints the assigned port as soon as the agent connects. Exit code 1 means timeout. If wait times out, run maui devflow diagnose to identify the issue. Check async shell output for build errors.
Android only — set up port forwarding after the agent connects:
adb reverse tcp:19223 tcp:19223 # Broker (lets agent in emulator reach host broker)
adb forward tcp:<port> tcp:<port> # Agent (lets CLI reach agent in emulator)To rebuild: repeat from Step 1. See references/troubleshooting.md if the build fails.
4. Inspect and Interact
Typical inspection flow: 1. maui devflow ui tree --depth 15 --fields "id,type,text,automationId" — tree with key fields only (depth 15 reaches most controls) 2. maui devflow ui tree --window 1 — filter to a specific window (0-based index) 3. maui devflow ui query --automationId "MyButton" — find specific elements 4. maui devflow ui query --type Entry --fields "id,text,automationId" — all Entry fields with specific fields 5. maui devflow ui element <id> — get full details (type, bounds, visibility, children) 6. maui devflow ui property <id> Text — read any property by name 7. maui devflow ui screenshot --output screen.png — visual verification (auto-scaled to 1x on HiDPI) 8. maui devflow ui screenshot --id <elementId> --output el.png — element-only screenshot 9. maui devflow ui screenshot --selector "Button" --output btn.png — screenshot by CSS selector
Property inspection is more reliable than screenshots for verifying exact runtime values:
maui devflow ui property <id> BackgroundColor # verify dark mode colors
maui devflow ui property <id> IsVisible # check element visibilityLive editing (no rebuild needed):
maui devflow ui set-property <id> TextColor "Tomato"
maui devflow ui set-property <id> FontSize "24"Supports: string, bool, int, double, Color (named/hex), Thickness, enums. Changes persist until the app restarts — safe for experimentation.
Typical interaction flow: 1. maui devflow ui fill --automationId "MyEntry" "text" — type into Entry/Editor fields (no query needed) 2. maui devflow ui tap --automationId "MyButton" — tap buttons, checkboxes, list items 3. maui devflow ui clear --automationId "MyEntry" — clear text fields 4. Or use element IDs from tree/query: maui devflow ui tap <elementId> 5. Take screenshot to verify result, or use --and-screenshot on the action
Blazor WebView (if applicable): 1. maui devflow webview snapshot — DOM tree as accessible text (best for AI) 2. maui devflow webview Input fill "css-selector" "text" — fill inputs 3. maui devflow webview Input dispatchClickEvent "css-selector" — click elements 4. maui devflow webview Runtime evaluate "js-expression" — run JS
Multiple BlazorWebViews: If the app has more than one BlazorWebView, each is registered independently with its AutomationId. Use webview webviews to list them, then target a specific one with --webview (or -w):
maui devflow webview webviews # list all WebViews
maui devflow webview -w BlazorLeft snapshot # snapshot of a specific WebView
maui devflow webview -w 1 Runtime evaluate "document.title" # target by indexWithout --webview, commands target the first (index 0) WebView.
Live CSS/DOM editing in Blazor (no rebuild needed):
maui devflow webview Runtime evaluate "document.querySelector('h1').style.color = 'tomato'"
maui devflow webview Runtime evaluate "document.documentElement.style.setProperty('--bg-color', '#1a1a2e')"5. Reading Application Logs
MauiDevFlow automatically captures all ILogger output and WebView console.* calls to rotating log files, retrievable remotely:
maui devflow logs # fetch 100 most recent log entries
maui devflow logs --limit 50 # fetch 50 entries
maui devflow logs --source webview # only WebView/Blazor console logs
maui devflow logs --source native # only native ILogger logs
maui devflow logs --follow # stream logs in real-time (Ctrl+C to stop)
maui devflow logs -f --source native # stream only native logs
maui devflow logs -f --json # stream as JSONL (machine-readable)Debugging workflow: Reproduce the issue → maui devflow logs --limit 20 → check for errors. Add temporary ILogger calls for more detail, rebuild, reproduce, and fetch logs again.
6. Screen Recording
Capture video of the app while performing interactions. Recording is host-side (not in-app) using platform-native tools.
# Start recording (default 30s timeout)
maui devflow ui recording start --output demo.mp4
# Interact with the app
maui devflow ui tap <buttonId>
maui devflow ui navigate "//blazor"
maui devflow ui fill <entryId> "Hello World"
# Stop and save
maui devflow ui recording stopPlatform tools used automatically:
- Android:
adb screenrecord(max 180s, capped with warning) - iOS Simulator:
xcrun simctl io recordVideo - Mac Catalyst / macOS (AppKit):
screencapture -v(targets app window when possible) - Windows/Linux:
ffmpeg(must be on PATH)
Options: --timeout <seconds> (default 30), --output <path> (default recording_<timestamp>.mp4). Only one recording at a time — stop before starting a new one.
7. Network Request Monitoring
Monitor HTTP requests made by the app in real-time. MauiDevFlow automatically intercepts all IHttpClientFactory-based HTTP traffic via a DelegatingHandler — no app code changes needed beyond the standard AddMauiDevFlowAgent() setup.
# Live monitor — streams requests as they happen (Ctrl+C to stop)
maui devflow ui network
# JSONL streaming — machine-readable, one JSON object per line
maui devflow ui network --json
# One-shot: list recent captured requests
maui devflow ui network list
# Filter by method or host
maui devflow ui network list --method POST
maui devflow ui network list --host api.example.com
# Full request/response details (headers + body)
maui devflow ui network detail <requestId>
# Clear captured requests
maui devflow ui network clearHow it works:
- A
DelegatingHandlerwraps the platform's HTTP handler (AndroidMessageHandler,
NSUrlSessionHandler, etc.), capturing request/response metadata, headers, and bodies
- Auto-injected via
ConfigureHttpClientDefaults— works for allIHttpClientFactoryclients - For
new HttpClient()outside DI, useDevFlowHttp.CreateClient()helper - Bodies up to 256KB are captured (configurable via
AgentOptions.MaxNetworkBodySize) - A ring buffer (default 500 entries) stores recent requests in-memory
JSONL output is ideal for AI parsing — pipe to jq or process programmatically:
maui devflow ui network --json | jq 'select(.statusCode >= 400)'WebSocket streaming: The live monitor uses WebSocket (/ws/network) for real-time push. Connecting clients receive a replay of buffered history, then live entries as they arrive.
8. App Storage (Preferences & Secure Storage)
Read, write, and delete app preferences and secure storage entries remotely. Useful for debugging state, resetting app configuration, or injecting test values.
# Preferences (typed key-value store)
maui devflow storage preferences list # list all known keys
maui devflow storage preferences get theme_mode # get a string value
maui devflow storage preferences get counter --type int # get a typed value
maui devflow storage preferences set api_url "https://dev.example.com"
maui devflow storage preferences set dark_mode true --type bool
maui devflow storage preferences delete temp_key
maui devflow storage preferences clear # clear all
# Shared preferences containers
maui devflow storage preferences list --sharedName settings
maui devflow storage preferences set key val --sharedName settings
# Secure Storage (encrypted, string values only)
maui devflow storage secure-storage get auth_token
maui devflow storage secure-storage set auth_token "eyJhbGc..."
maui devflow storage secure-storage delete auth_token
maui devflow storage secure-storage clearNote: Preference key listing uses an internal registry (keys set via the agent are tracked). Keys set directly in app code won't appear in list unless also set via the agent.
9. Platform Info & Device Features
Query read-only device and app state. These are one-shot snapshot reads.
maui devflow device app-info # app name, version, build, theme
maui devflow device device-info # manufacturer, model, OS, idiom
maui devflow device display # screen density, size, orientation
maui devflow device battery # charge level, state, power source
maui devflow device connectivity # WiFi/Cellular/Ethernet, network access
maui devflow device version-tracking # version history, first launch detection
maui devflow device permissions # check all common permission statuses
maui devflow device permissions camera # check a specific permission
maui devflow device geolocation # current GPS coordinates
maui devflow device geolocation --accuracy High --timeout 1510. Device Sensors
Start, stop, and stream real-time sensor data. Sensors auto-start when streaming.
maui devflow device sensors list # list sensors + status
maui devflow device sensors start accelerometer # start a sensor
maui devflow device sensors stop accelerometer
# Stream readings to stdout (JSONL)
maui devflow device sensors stream accelerometer # Ctrl+C to stop
maui devflow device sensors stream gyroscope --speed Game # higher frequency
maui devflow device sensors stream compass --duration 10 # stop after 10 secondsAvailable sensors: accelerometer, barometer, compass, gyroscope, magnetometer, orientation. Speed options: UI (default), Game, Fastest, Default.
WebSocket streaming: Sensor data uses WebSocket (/ws/sensors?sensor=<name>) for real-time push. Each reading is a JSON object with sensor, timestamp, and data fields.
Command Reference
maui devflow ui (Native Agent)
Global options (work on any subcommand):
--agent-host(default localhost),--agent-port(auto-discovered via broker),--platform--json— force JSON output. Auto-enabled when stdout is piped/redirected (TTY auto-detection).--no-json— force human-readable output even when piped.- Env var:
MAUIDEVFLOW_OUTPUT=jsonfor persistent JSON mode.
Implicit element resolution: Commands that take an <elementId> (tap, fill, clear, focus) also accept --automationId, --type, --text, --index to resolve the element in a single call. This eliminates the query→act round-trip. The <elementId> argument is optional when resolution options are provided.
Post-action flags: tap, fill, clear accept --and-screenshot [path], --and-tree, --and-tree-depth N to return verification data alongside the action result.
| Command | Description |
|---|---|
ui status [--window W] | Agent connection status, platform, app name, window count |
ui tree [--depth N] [--window W] [--fields F] [--format compact] | Visual tree. --fields "id,type,text" projects specific fields. --format compact returns only id, type, text, automationId, bounds |
| `ui query [--type T] [--automationId A] [--text T] [--selector S] [--fields F] [--format compact] [--wait-until exists\ | gone] [--timeout N]` |
ui hittest <x> <y> [--window W] | Find elements at a point (deepest first). Returns IDs, types, bounds |
ui tap [elementId] [--automationId A] [--type T] [--text T] [--index N] [--and-screenshot [path]] [--and-tree] [--and-tree-depth N] | Tap element by ID or implicit resolution |
ui fill [elementId] <text> [--automationId A] [--type T] [--text T] [--index N] [--and-screenshot [path]] [--and-tree] | Fill text into Entry/Editor. elementId optional when using resolution options |
ui clear [elementId] [--automationId A] [--type T] [--text T] [--index N] [--and-screenshot [path]] [--and-tree] | Clear text. elementId optional when using resolution options |
ui focus [elementId] [--automationId A] [--type T] [--text T] [--index N] | Set focus. elementId optional when using resolution options |
ui assert [--id ID] [--automationId A] <property> <expected> | Assert element property value. Exit 0 if match, 1 if mismatch. Ideal for verification without screenshots |
ui screenshot [--output path.png] [--window W] [--id ID] [--selector SEL] [--overwrite] [--max-width N] [--scale native] | PNG screenshot. Auto-scales to 1x logical resolution on HiDPI displays (2x, 3x). Use --scale native for full resolution. --max-width N overrides auto-scaling with explicit width. --overwrite replaces existing file |
ui property <elementId> <prop> | Read property (Text, IsVisible, FontSize, etc.) |
ui set-property <elementId> <prop> <value> | Set property (live editing — colors, text, sizes, etc.) |
ui element <elementId> | Full element JSON (type, bounds, children, etc.) |
ui navigate <route> | Shell navigation (e.g. //native, //blazor) |
ui scroll [--element id] [--dx N] [--dy N] [--item-index N] [--group-index N] [--position P] [--window W] | Scroll by delta, item index, or scroll element into view. --item-index scrolls to a specific item in CollectionView/ListView (works even for virtualized off-screen items). --position: MakeVisible (default), Start, Center, End. Delta scroll (--dy -500) uses native platform scroll for CollectionView |
ui resize <width> <height> [--window W] | Resize app window. Window is 0-based index; default first window |
ui logs [--limit N] [--skip N] [--source S] [--follow] | Fetch or stream application logs. --follow / -f streams in real-time (Ctrl+C to stop). Source: native, webview, or omit for all |
ui recording start [--output path] [--timeout 30] | Start screen recording. Default timeout 30s |
ui recording stop | Stop active recording and save the video file |
ui recording status | Check if a recording is currently in progress |
ui network | Live network monitor — streams HTTP requests in real-time (Ctrl+C to stop) |
ui network list [--host H] [--method M] | One-shot: dump recent captured HTTP requests |
ui network detail <requestId> | Full request/response details: headers, body, timing |
ui network clear | Clear the captured request buffer |
ui preferences list [--sharedName N] | List all known preference keys and values |
ui preferences get <key> [--type T] [--sharedName N] | Get a preference value. Types: string, int, bool, double, float, long, datetime |
ui preferences set <key> <value> [--type T] [--sharedName N] | Set a preference value |
ui preferences delete <key> [--sharedName N] | Remove a preference |
ui preferences clear [--sharedName N] | Clear all preferences |
ui secure-storage get <key> | Get a secure storage value |
ui secure-storage set <key> <value> | Set a secure storage value |
ui secure-storage delete <key> | Remove a secure storage entry |
ui secure-storage clear | Clear all secure storage entries |
ui platform app-info | App name, version, build, package, theme |
ui platform device-info | Device manufacturer, model, OS, idiom |
ui platform display | Screen density, size, orientation, refresh rate |
ui platform battery | Battery level, state, power source |
ui platform connectivity | Network access and connection profiles |
ui platform version-tracking | Current/previous/first version, build history, isFirstLaunch |
ui platform permissions [name] | Check permission status. Omit name to check all common permissions |
ui platform geolocation [--accuracy A] [--timeout N] | Get current GPS coordinates. Accuracy: Lowest, Low, Medium (default), High, Best |
ui sensors list | List available sensors and their current state (started/stopped) |
ui sensors start <sensor> [--speed S] | Start a sensor. Sensors: accelerometer, barometer, compass, gyroscope, magnetometer, orientation. Speed: UI (default), Game, Fastest, Default |
ui sensors stop <sensor> | Stop a sensor |
ui sensors stream <sensor> [--speed S] [--duration N] | Stream sensor readings via WebSocket. Duration 0 = indefinite (Ctrl+C to stop) |
commands [--json] | List all available commands with descriptions. --json returns machine-readable schema with command names, descriptions, and whether they mutate state |
Element IDs come from ui tree or ui query. AutomationId-based elements use their AutomationId directly. Others use generated hex IDs. When multiple elements share the same AutomationId, suffixes are appended: TodoCheckBox, TodoCheckBox_1, TodoCheckBox_2, etc.
Element ID lifecycle: IDs are ephemeral — they're regenerated on each tree walk. After navigation, page changes, or significant UI updates, re-query to get fresh IDs. AutomationIds are stable across rebuilds (they come from XAML), so prefer --automationId for scripted flows.
maui devflow webview (Blazor WebView CDP)
Global options: --agent-host (default localhost), --agent-port (auto-discovered via broker). CDP commands use the same agent port — all communication goes through a single port. Use --webview <id> (or -w <id>) on any CDP command to target a specific WebView by index, AutomationId, or element ID. Default: first WebView.
| Command | Description |
|---|---|
webview status | CDP connection status and WebView count |
webview webviews [--json] | List available CDP WebViews (index, AutomationId, ready status) |
webview snapshot | Accessible DOM text (best for AI agents) |
webview source | Get full page HTML source |
webview Browser getVersion | Browser/WebView version info |
webview Runtime evaluate <expr> | Evaluate JavaScript |
webview DOM getDocument | Full DOM document |
webview DOM querySelector <sel> | Find first matching element |
webview DOM querySelectorAll <sel> | Find all matching elements |
webview DOM getOuterHTML <sel> | Get outer HTML of element |
webview Page navigate <url> | Navigate to URL |
webview Page reload | Reload page |
webview Page captureScreenshot | Screenshot as base64 |
webview Input dispatchClickEvent <sel> | Click element by CSS selector |
webview Input insertText <text> | Insert text at focused element |
webview Input fill <selector> <text> | Focus + fill text into element |
Multi-WebView targeting: If the app has multiple BlazorWebViews, use webview webviews to list them, then --webview <index-or-automationId> on any command to target a specific one. Example: maui devflow webview --webview 1 snapshot or maui devflow webview -w MyWebView Runtime evaluate "1+1".
Blazor Hybrid CDP Interaction Limitations
WARNING: webview Input fill and webview Input dispatchClickEvent use synthetic DOM events. Blazor's event delegation checks event.isTrusted and ignores synthetic events. This means CDP input commands may silently fail in Blazor Hybrid apps.
Workaround hierarchy (try in order): 1. maui devflow ui tap --automationId "X" — native-level tap, generates real touch events 2. maui devflow ui fill --automationId "X" "text" — native-level text input 3. maui devflow webview Runtime evaluate "document.querySelector('button').click()" — may work for non-Blazor-delegated handlers 4. If the element has no AutomationId, use maui devflow ui hit-test <x> <y> to find it by coordinates from a screenshot
NEVER spend more than 5 minutes on CDP input failures. Fall back to MAUI-level interaction immediately.
Navigating Within Blazor Hybrid Apps
Blazor Hybrid navigation works differently from web Blazor. The URL bar is not the source of truth — NavigationManager is.
Approach hierarchy (try in order): 1. Tap the nav element — Find the link/button in the MAUI tree or CDP snapshot that navigates to the target page and tap it. This is the most reliable approach. 2. Use CDP to invoke Blazor's NavigationManager:
maui devflow webview Runtime evaluate "Blazor.navigateTo('/vocab-quiz')"If this returns undefined, inspect the API:
maui devflow webview Runtime evaluate "JSON.stringify(Object.keys(Blazor))"3. Navigate via the app's own UI — Use maui devflow webview snapshot to find the page, locate the nav link, and click through the app's normal flow.
NEVER brute-force JS navigation for more than 3 attempts. If Blazor.navigateTo and tapping links both fail, something is fundamentally wrong with the app state — diagnose that instead.
maui devflow Broker & Discovery
The broker is a background daemon that manages port assignments for all running agents. The CLI auto-starts the broker on first use — no manual setup needed.
⚠️ Broker Idle Timeout: The broker shuts down automatically after a period of inactivity (no connected agents and no CLI commands). If you return to debugging after a break and get connection errors, the broker likely timed out. It will auto-restart on the next CLI command, but any previously connected agents will need to reconnect (restart the app).
| Command | Description |
|---|---|
list | Show all registered agents (ID, app, platform, TFM, port, uptime) |
wait [--timeout 120] [--project path] [--wait-platform P] [--json] | Wait for an agent to connect. Outputs the port (or JSON with --json). Useful after dotnet build -t:Run to block until the app is ready |
broker status | Broker daemon status and connected agent count |
broker start | Start broker daemon (auto-started by CLI — rarely needed manually) |
broker stop | Stop broker daemon |
broker log | Show broker log file |
maui devflow batch (Multi-Command Execution)
Execute multiple commands in one invocation via stdin. Returns JSONL responses. Use for multi-step interactions to avoid repeated port resolution.
echo "MAUI fill textUsername user; MAUI fill textPassword pwd123; MAUI tap buttonLogin" | maui devflow batchFor full options, JSONL format, and streaming details, see references/batch.md.
Device Data Extraction (Physical Devices)
Use xcrun devicectl to pull/push files from physical iOS devices — essential for debugging database issues, recovering user data, or inspecting app state on a real phone.
# Pull SQLite DB from app container
xcrun devicectl device copy from \
--device <UDID> \
--domain-type appDataContainer \
--domain-identifier <BUNDLE_ID> \
--source Library/Application\ Support/sentencestudio/sentencestudio.db \
--destination ./pulled-db/⚠️ Always handle WAL files when pulling/pushing SQLite databases. See references/ios-and-mac.md for the complete workflow including WAL checkpoint and empty-file push.
Platform Details
For detailed platform-specific setup, simulator/emulator management, and troubleshooting:
- Setup & Installation: See references/setup.md
- iOS / Mac Catalyst: See references/ios-and-mac.md
- macOS (AppKit): See references/macos.md
- Android: See references/android.md
- Linux / GTK: See references/linux.md
- Troubleshooting: See references/troubleshooting.md
iOS Device Install on DX24 (SentenceStudio-specific)
When installing to DX24 (Captain's iPhone 15 Pro, device ID CF4F94E3-A1C9-5617-A089-9ABB0110A09F) via xcrun devicectl device install, the first attempt frequently fails with NWError 57 / CoreDeviceError 4000 because the device's CoreDevice control-channel tunnel is killed when the device enters deep sleep. Wake + unlock the device first; budget for one retry on the install command. This is a known pattern, not a build error.
Full procedure, evidence, and recipe: `.squad/skills/maui-ios-dx24-install/SKILL.md`.
⚠️ Non-Disruptive Operation
CRITICAL: Never run commands that steal focus, move windows, simulate mouse/keyboard input, or otherwise disrupt the user's desktop. The user is likely working on the same computer.
Never use:
osascriptto focus/activate windows, click UI elements, or send keystrokesscreencaptureinteractively (the MauiDevFlow screenshot command captures in-process instead)xdotoolfocus/activate/key commands that affect the active window- Any command that moves the mouse cursor or simulates input at the OS level
open -ato bring apps to the foreground (useopenonly to launch, not to focus)
Instead: All inspection and interaction goes through maui devflow CLI commands, which communicate with the in-app agent over HTTP — no foreground focus required. If you need something that would require OS-level control (e.g., dismissing a system dialog outside the app), ask the user to do it manually rather than attempting automation that would hijack their input.
Tips
- `maui devflow list` shows runtime state, not project integration. Empty list ≠ "not installed."
Always check project files (grep -r MauiDevFlow *.csproj) before concluding DevFlow is unavailable.
- `maui devflow diagnose` is the fastest way to check the entire chain: CLI → broker → agents → projects.
- After launching through Aspire, always run
maui devflow waitbefore attempting any interaction. - Use `maui devflow batch` for multi-step interactions — resolves port once, adds delays,
returns structured JSONL. See references/batch.md.
- Always use `maui devflow ui screenshot` — captures in-process, app does NOT need
foreground focus.
- Use
AutomationIdon important MAUI controls for stable element references. - For Blazor Hybrid,
webview snapshotis the most AI-friendly way to read page state. - Port discovery, multi-project setup, and custom ports: see references/setup.md.
- Shell apps: Read
AppShell.xamlto discover routes before navigating. Routes are
case-sensitive and often lowercase.
- CollectionView items: Tap the container Grid/StackLayout, not inner Labels/Images.
Use --item-index to scroll to off-screen items.
- Ambiguous `--text`: When text appears on multiple pages, use explicit IDs from
tree.
AI Agent Best Practices
Output Format
- Always use `--json` or rely on TTY auto-detection (JSON is auto-enabled when stdout is piped/redirected).
- Set
MAUIDEVFLOW_OUTPUT=jsonin your environment for consistent machine-readable output. - Use
--no-jsononly when you specifically need human-readable output in a pipe. - Errors go to stderr as structured JSON:
{"error": "...", "type": "RuntimeError", "retryable": false, "suggestions": [...]}. - Check exit codes: 0 = success, non-zero = failure.
Reducing Token Usage
- Use `--depth 15` (or higher) for
ui tree— MAUI visual trees are deeply nested (a simple
control is often at depth 10-15). Start with --depth 15; if you see truncated children, increase. After your first successful tree dump, note the depth where meaningful controls appear and reuse that depth for subsequent calls. If the tree is still too large, combine with --fields to reduce width.
- Use `--fields "id,type,text,automationId"` to project only the fields you need.
- Use `--format compact` for minimal tree output (id, type, text, automationId, bounds).
- Prefer `ui query --automationId` over full tree traversal — much smaller response.
- Use element-level screenshots (
--id <elementId>) when you only need to see one control.
Adaptive Depth Learning
MAUI app trees vary in depth — a simple app might have controls at depth 8, while a complex app with Shell + NavigationPage + nested layouts might need depth 20+. After your first ui tree call, look at where the leaf-level controls (Button, Entry, Label) appear and remember that depth. Use it for all subsequent tree calls in the same session. If you navigate to a new page that seems deeper, bump the depth up. This avoids both truncating useful content and wasting tokens on excessively deep dumps.
Screenshot Auto-Scaling (HiDPI)
Screenshots are automatically scaled to 1x logical resolution by default. The agent detects the device's display density (2x on Retina, 3x on iPhone Pro Max, 1x on desktop) and divides the screenshot dimensions accordingly. This happens server-side before transfer.
- No action needed — just use
maui devflow ui screenshot --output screen.pngand the
image will be appropriately sized for AI understanding.
- Full resolution: Use
--scale nativewhen you need pixel-perfect images (e.g., verifying
exact colors, alignment, or anti-aliasing).
maui devflow ui screenshot --output full-res.png --scale native- Explicit max width: Use
--max-width Nto override auto-scaling with a specific pixel width.
maui devflow ui screenshot --output screen.png --max-width 600Eliminating Round-Trips
- Use implicit resolution instead of query-then-act:
# Instead of: query → get ID → tap
maui devflow ui tap --automationId "LoginButton"
maui devflow ui fill --automationId "Username" "admin"
maui devflow ui tap --type Button --index 0 # first Button- Use `--wait-until` instead of polling loops:
maui devflow ui query --automationId "ResultsList" --wait-until exists --timeout 10
maui devflow ui query --automationId "Spinner" --wait-until gone --timeout 30- Use post-action flags to verify in one call:
maui devflow ui tap abc123 --and-screenshot --and-tree --and-tree-depth 5- Use `ui assert` for quick state checks:
maui devflow ui assert --id abc123 Text "Welcome!"
maui devflow ui assert --automationId "Counter" Text "5"Element IDs
- Element IDs are ephemeral — re-query after navigation or state changes.
- Don't cache element IDs across multiple actions — refresh with
treeorquery. - Prefer
--automationIdfor stable references (set in XAML). - Use
maui devflow commands --jsonto discover available commands at runtime.
Shell Navigation
- Routes are case-sensitive and come from
ShellContent Route=""in XAML, not from
FlyoutItem Title. Discover routes by reading AppShell.xaml:
grep -i 'Route=' AppShell.xaml- Flyout menu items use generated IDs like
FlyoutItem_D_FAULT_FlyoutItem0. Find them
at the top level of the tree output. Don't try to tap Labels inside flyout items.
- Flyout dismissal: After tapping a flyout item, the flyout may stay open. Dismiss with:
maui devflow ui set-property <shellId> FlyoutIsPresented "false"CollectionView / ListView
- Tapping items: Always tap the item's container (Grid/StackLayout), not inner elements
(Label/Image). The item template's root element handles selection.
- Virtualization: CollectionView/ListView use item virtualization — only visible items
(plus a small buffer) exist in the visual tree. Off-screen items have NO visual element. The tree shows itemCount in the CollectionView's properties so you know total items.
- Scrolling by item index (best for reaching off-screen items):
maui devflow ui scroll --element <cvId> --item-index 20 --position CenterThis works even for items not in the tree yet — the platform scrolls to materialize them.
- Scrolling by pixel delta (for fine-grained scrolling):
maui devflow ui scroll --element <cvId> --dy -500Uses native platform scroll (UIScrollView/RecyclerView) — works on CollectionView.
- Workflow: Get tree → note
itemCount→ scroll by index → re-query tree → interact:
maui devflow ui tree --depth 15 # CollectionView shows itemCount: 25
maui devflow ui scroll --item-index 20
maui devflow ui tree --depth 15 # items around index 20 now visibleImplicit Resolution Gotchas
- `--text` searches the entire visual tree, including hidden pages (other Shell tabs).
If the text is ambiguous (e.g., "+", "OK", "Cancel"), it may match a wrong element on a different page.
- Prefer `--automationId` for reliable targeting. Fall back to explicit element IDs from
tree/query for elements without AutomationIds.
- Use `--type` + `--text` together to narrow matches when text alone is ambiguous.
Canonical Workflows
Login flow:
maui devflow ui query --automationId "LoginPage" --wait-until exists --timeout 15
maui devflow ui fill --automationId "UsernameField" "admin"
maui devflow ui fill --automationId "PasswordField" "password"
maui devflow ui tap --automationId "LoginButton" --and-screenshot
maui devflow ui query --automationId "HomePage" --wait-until exists --timeout 10Shell navigation:
# Discover routes from XAML
grep -i 'Route=' AppShell.xaml # find route names
maui devflow ui navigate "//home" # navigate to a route
maui devflow ui tap FlyoutButton # open flyout
maui devflow ui tree --depth 3 --fields "id,type,text" # find flyout items
maui devflow ui tap <flyoutItemId> # tap item
maui devflow ui set-property <shellId> FlyoutIsPresented "false" # dismiss flyoutElement inspection:
maui devflow ui query --automationId "MyControl" --json --fields "id,type,text,bounds"
maui devflow ui element <id> --json
maui devflow ui property <id> TextState verification:
maui devflow ui tap --automationId "IncrementButton"
maui devflow ui assert --automationId "CounterLabel" Text "1"Circuit Breaker: When to Stop and Reassess
Apply these time limits to any single approach:
| Task | Max attempts | Max time | Then do |
|---|---|---|---|
| CDP command fails | 3 tries | 5 min | Fall back to MAUI-level commands |
| MAUI-level command fails | 3 tries | 5 min | Check maui devflow diagnose |
| Build fails | 2 tries | 10 min | Clean and rebuild from scratch |
| Navigation fails | 3 tries | 5 min | Use the app's own UI to navigate |
| Any approach | — | 15 min | STOP. Summarize what failed. Ask the user. |
After any failure: Diagnose FIRST, then retry.
maui devflow diagnose— full system health checkmaui devflow logs --limit 20— app-side errorsmaui devflow webview status— CDP connection healthmaui devflow ui status— agent connection health
The 15-minute rule: If you have spent 15 minutes total on testing without a single successful interaction, something is fundamentally wrong with the setup. Stop trying workarounds and diagnose the environment.
Verification Integrity
NEVER claim verification without evidence. For each surface tested, provide:
- Platform and device/simulator identifier (e.g., "iPhone 16 Pro — iOS 26.2 sim, UDID 802B6FB8")
- Screenshot or snapshot proving the feature works (filename or inline)
- What was NOT tested and why — explicitly state gaps
A test that fails is a "failed test," not a "verified feature." "Attempted" and "verified" are NOT interchangeable:
- Verified = observed the expected behavior with captured evidence
- Attempted = tried but could not confirm the outcome
If simulator or device testing fails, report honestly: "Verified on web only. Simulator testing failed due to [reason]. The feature is NOT verified on native."
{
"commit": "8a89c0b1c0c8711b59a3ed47000ab3cd1191f141",
"updatedAt": "2026-02-26T00:23:35.4101030Z",
"branch": "main"
}Android Reference
Table of Contents
Emulator Management
Avoiding multi-project conflicts
When multiple projects (or AI agents) may deploy to Android emulators simultaneously, each project should use its own dedicated AVD. Two apps deployed to the same emulator will coexist (unlike iOS), but adb reverse/adb forward port forwarding is per-device and can cause confusion when multiple emulators are running.
Before creating or starting an emulator, check what's already in use:
maui devflow list # shows agents with platform + port
adb devices # shows connected emulatorsIf an emulator is already running another project's agent, create a new AVD:
android avd create --name "ProjectName-Pixel8" \
--sdk "system-images;android-35;google_apis;arm64-v8a" --device pixel_8
android avd start --name "ProjectName-Pixel8"When multiple emulators are running, use -s <serial> to target a specific one:
adb -s emulator-5554 reverse tcp:19223 tcp:19223 # first emulator
adb -s emulator-5556 reverse tcp:19223 tcp:19223 # second emulatorNaming convention: Use <ProjectName>-<DeviceType> (e.g. TodoApp-Pixel8) so it's clear which AVD belongs to which project.
List and start AVDs
android avd list # list available AVDs
android avd start --name <avd-name> # start emulatorCreate AVD
# List available targets and device profiles
android avd targets # system images
android avd devices # device profiles (pixel, etc.)
android avd create --name "Pixel8API35" \
--sdk "system-images;android-35;google_apis;arm64-v8a" \
--device pixel_8Delete AVD
android avd delete --name <avd-name>Verify emulator is running
adb devices # should show "emulator-5554 device"
android device list # formatted listBuilding and Deploying
# Build and deploy to running emulator
dotnet build -f net10.0-android -t:Run
# Build only (no deploy)
dotnet build -f net10.0-androidCritical: Port forwarding after deploy — the Android emulator runs in its own network. Forward the broker port and the agent port:
adb reverse tcp:19223 tcp:19223 # Broker (lets agent register)
adb forward tcp:<port> tcp:<port> # Agent (lets CLI reach agent)The broker reverse is needed so the agent inside the emulator can connect to the host's broker daemon. The agent forward uses the port shown in maui devflow list after the agent registers (range 10223–10899).
If the broker isn't available (fallback mode), forward the port from .mauidevflow instead:
adb reverse tcp:9223 tcp:9223 # Fallback: direct agent portThen verify: maui devflow ui status and maui devflow webview status.
Install APK manually
adb install -r path/to/app.apk # install/reinstall
android device install --package path/to/app.apkAndroid CLI Tool
The android command (from androidsdk.tool NuGet) wraps SDK tools.
SDK management
android sdk list # all packages
android sdk list --installed # installed only
android sdk list --available # available for install
android sdk install --package "platforms;android-35"
android sdk install --package "system-images;android-35;google_apis;arm64-v8a"
android sdk install --package "emulator"
android sdk uninstall --package <package-name>
android sdk info # SDK location, tools versions
android sdk accept-licenses # accept all SDK licenses
android sdk download # download cmdline-toolsAVD management
android avd list # available AVDs
android avd targets # available system images
android avd devices # available device profiles
android avd create --name <name> --sdk <system-image> --device <device>
android avd delete --name <name>
android avd start --name <name>Device/emulator operations
android device list # connected devices/emulators
android device info [--device <serial>] # device properties
android device install --package <apk> # install APK
android device uninstall --package <pkg-id> # uninstall by package nameJDK management
android jdk list # available JDKs
android jdk info # current JDK infoADB Reference
Device/emulator basics
adb devices # list connected devices
adb -s <serial> shell # shell into specific device
adb shell pm list packages | grep <name> # find installed packages
adb shell am start -n <pkg>/<activity> # launch activity
adb shell am force-stop <pkg> # kill appPort forwarding (critical for MauiDevFlow)
adb reverse tcp:19223 tcp:19223 # Broker (agent → host)
adb forward tcp:<port> tcp:<port> # Agent (CLI → emulator, get port from `maui devflow list`)
adb reverse --list # verify forwarding
adb forward --list # verify forwarding
adb reverse --remove-all # clean up reverse
adb forward --remove-all # clean up forwardFile operations
adb push local/file /sdcard/path # push file to device
adb pull /sdcard/path local/file # pull file from deviceLogs
adb logcat -s "DOTNET" --format brief # .NET runtime logs
adb logcat -s "MauiDevFlow" # agent logs
adb logcat --pid=$(adb shell pidof <pkg>) # app-specific logs
adb logcat -c # clear log bufferScreenshots and screen recording
adb shell screencap /sdcard/screen.png && adb pull /sdcard/screen.png
adb shell screenrecord /sdcard/video.mp4 # Ctrl+C to stopSDK Management
Typical setup for MAUI Android development
android sdk accept-licenses
android sdk install --package "platforms;android-35"
android sdk install --package "build-tools;35.0.0"
android sdk install --package "system-images;android-35;google_apis;arm64-v8a"
android sdk install --package "emulator"
android sdk install --package "platform-tools"Environment variables
export ANDROID_HOME=$HOME/Library/Android/sdk
export ANDROID_SDK_ROOT=$ANDROID_HOME
export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulatorTroubleshooting
- `adb devices` shows "unauthorized": Accept the USB debugging prompt on the device/emulator.
- Agent not connecting on emulator: Forgot
adb reverse tcp:19223 tcp:19223for the broker. Run port forwarding, then checkmaui devflow list. - Emulator won't start: Check available system images with
android avd targets. May need
to install with android sdk install --package "system-images;...".
- Build error "No Android devices found": Ensure emulator is booted (
adb devices). - Slow emulator: Use hardware acceleration. Prefer
arm64-v8aimages on Apple Silicon Macs.
Batch Command Reference
Execute multiple MAUI/cdp commands in a single CLI invocation via stdin. Outputs JSONL responses (one JSON object per line) to stdout — ideal for AI agents and scripting.
Usage
# Pipe multiple commands (semicolons or newlines as separators)
echo "MAUI fill textUsername user; MAUI fill textPassword pwd123; MAUI tap buttonLogin" | maui devflow batch
# Multi-line input
printf "MAUI status\nMAUI tree\nMAUI screenshot --output screen.png" | maui devflow batch
# With options
echo "MAUI status; MAUI tree" | maui devflow batch --delay 500 --continue-on-error --agent-port 10224
# Human-readable output instead of JSONL
echo "MAUI status; MAUI tree" | maui devflow batch --humanOptions
| Option | Default | Description |
|---|---|---|
--delay <ms> | 250 | Delay between commands (lets UI settle) |
--continue-on-error | false | Continue after a command fails (default: stop) |
--human | false | Human-readable output instead of JSONL |
JSONL Response Format
One JSON object per command, streamed as each completes:
{"command":"MAUI fill textUsername user","exit_code":0,"output":"Filled: textUsername"}
{"command":"MAUI tap buttonLogin","exit_code":1,"output":"Error: Element not found: buttonLogin"}Interactive Streaming
The batch command processes stdin line-by-line, so a caller can read each JSONL response before sending the next command. This enables reactive workflows where the AI agent inspects results and decides the next action.
Input Rules
- Lines starting with
#are comments (skipped) - Empty lines are skipped
- Semicolons separate multiple commands on one line
- Quoted strings are preserved:
ui fill myEntry "hello world" - Only
MAUIandcdpcommands are allowed (broker/list/etc. are rejected)
{
"lastSuccessful": null,
"lastFailed": null
}
iOS & Mac Catalyst Reference
Table of Contents
- Simulator Management
- Physical Device Data Extraction
- Building and Deploying
- Apple CLI Tool
- xcrun simctl Reference
- Troubleshooting
Simulator Management
Avoiding multi-project conflicts
When multiple projects (or AI agents) may deploy to iOS simulators simultaneously, each project should use its own dedicated simulator. Two apps deployed to the same simulator will replace each other — only the last-deployed app survives.
Before creating or booting a simulator, check what's already in use:
maui devflow list # shows agents with platform + port
xcrun simctl list devices booted # shows all booted simulatorsIf a booted simulator is already running another project's agent, create a new one:
xcrun simctl create "ProjectName-iPhone17Pro" "iPhone 17 Pro" "iOS 26.2"
# Use the returned UDID in your build commandNaming convention: Use <ProjectName>-<DeviceType> (e.g. TodoApp-iPhone17Pro) so it's clear which simulator belongs to which project.
List simulators
xcrun simctl list devices # all devices by runtime
xcrun simctl list devices booted # only booted
xcrun simctl list devices available # only available
apple simulator list # formatted table
apple simulator list --booted # booted onlyCreate simulator
# List available device types and runtimes first
xcrun simctl list devicetypes # e.g. "iPhone 16 Pro"
xcrun simctl list runtimes # e.g. "iOS 18.2"
xcrun simctl create "My iPhone" "iPhone 16 Pro" "iOS 18.2"
apple simulator create "My iPhone" --device-type "iPhone 16 Pro" --runtime "iOS 18.2"Boot / shutdown
xcrun simctl boot <UDID>
xcrun simctl shutdown <UDID>
apple simulator boot <UDID>
apple simulator shutdown <UDID>Install and launch app
xcrun simctl install booted /path/to/App.app
xcrun simctl launch booted com.company.appidScreenshots (iOS Simulator)
xcrun simctl io booted screenshot output.png
apple simulator screenshot <UDID> --output output.pngScreenshots (Mac Catalyst)
Use `maui devflow ui screenshot` for Mac Catalyst apps — it captures the UI in-process and does NOT require the app to be in the foreground. Never use osascript to bring the window to the front or screencapture for Mac Catalyst screenshots; they are unnecessary and unreliable.
maui devflow ui screenshot --output screen.pngDelete / erase
xcrun simctl erase <UDID> # factory reset
xcrun simctl delete <UDID> # permanently remove
xcrun simctl delete unavailable # clean up old simsPhysical Device Data Extraction
Use xcrun devicectl to pull and push files from physical iOS devices. This is essential for debugging data issues, inspecting SQLite databases, or recovering user data from a phone.
Prerequisites
- Device must be connected via USB or paired over WiFi
- Device must be unlocked and trusted
- Xcode 15+ required (devicectl was introduced in Xcode 15)
List connected devices
xcrun devicectl list devicesPull files from app container
# Pull a SQLite database from the app's data container
xcrun devicectl device copy from \
--device <DEVICE_UDID> \
--domain-type appDataContainer \
--domain-identifier <BUNDLE_ID> \
--source Library/Application\ Support/sentencestudio/sentencestudio.db \
--destination ./pulled-db/
# Pull the WAL and SHM files too (critical for complete data)
xcrun devicectl device copy from \
--device <DEVICE_UDID> \
--domain-type appDataContainer \
--domain-identifier <BUNDLE_ID> \
--source Library/Application\ Support/sentencestudio/sentencestudio.db-wal \
--destination ./pulled-db/
xcrun devicectl device copy from \
--device <DEVICE_UDID> \
--domain-type appDataContainer \
--domain-identifier <BUNDLE_ID> \
--source Library/Application\ Support/sentencestudio/sentencestudio.db-shm \
--destination ./pulled-db/Key parameters:
--domain-type appDataContainer— access the app's sandboxed data directory--domain-identifier— the app's bundle ID (e.g.,com.simplyprofound.sentencestudio)--source— path relative to the app container root
Push files to app container
xcrun devicectl device copy to \
--device <DEVICE_UDID> \
--domain-type appDataContainer \
--domain-identifier <BUNDLE_ID> \
--source ./modified.db \
--destination Library/Application\ Support/sentencestudio/sentencestudio.db⚠️ WAL File Handling (CRITICAL)
SQLite uses Write-Ahead Logging (WAL). When modifying and pushing a database back to the device:
1. Before pulling: Ideally, close the app first to flush WAL to main DB. 2. After modifying: Run PRAGMA wal_checkpoint(TRUNCATE) on the modified database to merge WAL into the main file and truncate the WAL:
sqlite3 ./pulled-db/sentencestudio.db "PRAGMA wal_checkpoint(TRUNCATE);"3. After pushing the modified DB: Push empty WAL and SHM files to prevent the app from reading stale WAL data:
# Create empty WAL/SHM files
touch ./empty.wal ./empty.shm
# Push the modified database
xcrun devicectl device copy to ... --source ./modified.db --destination .../sentencestudio.db
# Push empty WAL to overwrite stale WAL
xcrun devicectl device copy to ... --source ./empty.wal --destination .../sentencestudio.db-wal
# Push empty SHM
xcrun devicectl device copy to ... --source ./empty.shm --destination .../sentencestudio.db-shmIf you skip this step, the app may read stale WAL entries on next launch, causing data corruption or reverting your changes.
Building and Deploying
Mac Catalyst
dotnet build -f net10.0-maccatalyst # build only
dotnet build -f net10.0-maccatalyst -t:Run # build + run
open path/to/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/AppName.app # run existingiOS Simulator
# Find UDID of booted simulator
UDID=$(xcrun simctl list devices booted -j | python3 -c "
import json,sys
d=json.load(sys.stdin)
for r in d['devices'].values():
for dev in r:
if dev['state']=='Booted': print(dev['udid']); break
" 2>/dev/null | head -1)
# Build and deploy
dotnet build -f net10.0-ios -t:Run -p:_DeviceName=:v2:udid=$UDIDThe -t:Run target keeps the process alive while the app runs — it blocks until the app exits. Always run in an async/background shell, then poll maui devflow ui status to detect when the app is ready. Do NOT wait for the process to finish.
Determining the correct TFM
Check the project file for <TargetFrameworks>:
grep -i TargetFramework *.csprojCommon values: net9.0-ios, net9.0-maccatalyst, net10.0-ios, net10.0-maccatalyst.
Apple CLI Tool
The apple command (from appledev.tools NuGet) provides higher-level wrappers.
Simulator commands
apple simulator list [--booted|--available|--unavailable|--name "..."]
apple simulator create <name> --device-type "..." [--runtime "..."]
apple simulator boot <target>
apple simulator shutdown <target>
apple simulator erase <target>
apple simulator delete <target>
apple simulator screenshot <target> [--output path.png]
apple simulator app install <target> <app-path>
apple simulator app launch <target> <bundle-id>
apple simulator app uninstall <target> <bundle-id>
apple simulator open [<target>]
apple simulator open-url <target> <url>
apple simulator logs <target> [--filter "..."]
apple simulator push <target> <bundle-id> [--payload "..."]
apple simulator location set <target> --lat <lat> --lon <lon>
apple simulator privacy grant <target> <service> <bundle-id>Device commands
apple device list
apple xcode list # installed Xcode versionsxcrun simctl Reference
Key subcommands beyond the basics:
| Command | Use |
|---|---|
simctl addmedia <UDID> file.jpg | Add photos/videos to sim |
simctl openurl <UDID> "url" | Open URL / deep link |
simctl push <UDID> bundle payload.json | Simulate push notification |
simctl privacy <UDID> grant location bundle | Grant permissions |
simctl location <UDID> set 37.33,-122.03 | Set GPS location |
simctl pbcopy <UDID> | Copy stdin to clipboard |
simctl pbpaste <UDID> | Read clipboard |
simctl get_app_container <UDID> bundle | App container path |
simctl listapps <UDID> | Installed apps |
Troubleshooting
- Mac Catalyst blank/white screen after crash: macOS shows a "reopen windows" dialog after
a crash, blocking the app from rendering. All MAUI elements appear as [hidden] [disabled] with -1x-1 sizes. Fix: clear saved state before launch:
rm -rf ~/Library/Saved\ Application\ State/<bundle-id>.savedStateOr detect and dismiss via AppleScript:
osascript -e 'tell application "System Events" to tell process "AppName" to click button "Reopen" of window 1'- "Unable to lookup in current state: Shutdown": Simulator not booted. Run
xcrun simctl boot <UDID>. - Build error NETSDK1005 "Assets file doesn't have a target": Wrong TFM. Check
<TargetFrameworks> in .csproj and use matching version (e.g. net10.0-ios not net9.0-ios).
- Agent not connecting after deploy: The app may still be launching. Poll
maui devflow ui status every few seconds. If it hasn't connected after ~60-90s, read the async shell output from dotnet build -t:Run for build/launch errors.
- Mac Catalyst app name vs binary name: The
.appbundle name may differ from the project
name (e.g. MauiTodo.app vs SampleMauiApp). Check the ApplicationTitle in .csproj. Find the bundle: find bin/Debug/net10.0-maccatalyst -name "*.app" -maxdepth 3
Permission & Dialog Handling
Pre-grant permissions (prevents dialogs from appearing)
# Grant specific permission before the app requests it
xcrun simctl privacy <UDID> grant location com.company.appid
xcrun simctl privacy <UDID> grant camera com.company.appid
xcrun simctl privacy <UDID> grant photos com.company.appid
xcrun simctl privacy <UDID> grant contacts com.company.appid
xcrun simctl privacy <UDID> grant microphone com.company.appid
# Grant all permissions at once
xcrun simctl privacy <UDID> grant all com.company.appid
# Revoke (deny) a permission
xcrun simctl privacy <UDID> revoke location com.company.appid
# Reset (next request will show dialog again)
xcrun simctl privacy <UDID> reset all com.company.appid
# Via apple CLI
apple simulator privacy grant <UDID> location com.company.appidAvailable services: all, calendar, contacts, contacts-limited, location, location-always, photos, photos-add, media-library, microphone, motion, reminders, siri.
Using MauiDevFlow.Driver for permissions
var driver = new iOSSimulatorAppDriver();
driver.DeviceUdid = "<UDID>";
driver.BundleId = "com.company.appid";
// Pre-grant before running the app
await driver.GrantPermissionAsync(PermissionService.Location);
await driver.GrantPermissionAsync(PermissionService.Camera);
// Reset to test the dialog flow
await driver.ResetPermissionAsync(PermissionService.Location);Detecting and dismissing alerts (accessibility tree + HID tap)
When a dialog appears unexpectedly (permission prompt, app alert, action sheet), the driver can detect it via the iOS accessibility tree and tap a button to dismiss it:
// Check if an alert is currently showing
var alert = await driver.DetectAlertAsync();
if (alert is not null)
{
Console.WriteLine($"Alert: {alert.Title}");
foreach (var btn in alert.Buttons)
Console.WriteLine($" Button: {btn.Label} at ({btn.CenterX},{btn.CenterY})");
}
// Dismiss by tapping the first "accept" button (Allow, OK, etc.)
await driver.DismissAlertAsync();
// Dismiss by tapping a specific button
await driver.DismissAlertAsync("Don't Allow");
// Convenience: detect + dismiss if present, no-op if not
await driver.HandleAlertIfPresentAsync();Example workflow: permission dialog handling
1. App requests location → system shows "Allow location?" dialog
2. Agent detects dialog via DetectAlertAsync()
3. Agent sees buttons: ["Allow While Using App", "Allow Once", "Don't Allow"]
4. Agent taps "Allow While Using App" via DismissAlertAsync("Allow While Using App")
5. App receives permission grant, continues normal flowDialog test page in SampleMauiApp
The SampleMauiApp includes a Dialogs tab with buttons that trigger:
- Permission dialogs: Location, Camera, Photos, Contacts, Microphone, Notifications
- App alerts: OK-only, OK/Cancel, custom buttons (Delete/Keep)
- Action sheets: Multiple options with cancel/destructive
- Prompt dialogs: Text input with OK/Cancel
Use these to test and validate dialog detection and dismissal workflows.
Dark Mode Testing
Toggle dark mode
# macOS (affects Mac Catalyst apps)
osascript -e 'tell application "System Events" to tell appearance preferences to set dark mode to true'
osascript -e 'tell application "System Events" to tell appearance preferences to set dark mode to false'
# iOS Simulator
xcrun simctl ui <UDID> appearance dark
xcrun simctl ui <UDID> appearance lightVerify dark mode via inspection
Use maui devflow to verify colors without relying on screenshots:
maui devflow ui property <elementId> BackgroundColor # check MAUI element colors
maui devflow webview Runtime evaluate "window.matchMedia('(prefers-color-scheme: dark)').matches" # BlazorLinux / GTK Platform Guide
Platform-specific setup and usage for .NET MAUI apps running on Linux via Maui.Gtk (GTK4).
Overview
Maui.Gtk apps target net10.0 (not a platform-specific TFM like net10.0-ios) and use GTK4 via GirCore bindings. MauiDevFlow provides dedicated Linux packages that work with this architecture.
NuGet Packages
| Package | Purpose |
|---|---|
Redth.MauiDevFlow.Agent.Gtk | In-app agent (visual tree, screenshots, tapping, logging) |
Redth.MauiDevFlow.Blazor.Gtk | CDP bridge for WebKitGTK BlazorWebView |
These replace Redth.MauiDevFlow.Agent and Redth.MauiDevFlow.Blazor which target standard MAUI platforms (iOS, Android, macCatalyst, Windows).
<ItemGroup>
<PackageReference Include="Redth.MauiDevFlow.Agent.Gtk" Version="*" />
<!-- Blazor Hybrid apps also need: -->
<PackageReference Include="Redth.MauiDevFlow.Blazor.Gtk" Version="*" />
</ItemGroup>Registration
MauiProgram.cs
using MauiDevFlow.Agent.Gtk;
using MauiDevFlow.Blazor.Gtk; // Blazor Hybrid only
var builder = MauiApp.CreateBuilder();
// ... your existing setup ...
#if DEBUG
builder.AddMauiDevFlowAgent();
builder.AddMauiBlazorDevFlowTools(); // Blazor Hybrid only
#endifApplication Startup
The agent must be started after the MAUI Application is available. In your GTK app startup (e.g., GtkMauiApplication.OnActivate or equivalent):
#if DEBUG
app.StartDevFlowAgent();
// For Blazor Hybrid, wire CDP to the agent:
var blazorService = app.Handler?.MauiContext?.Services
.GetService<GtkBlazorWebViewDebugService>();
blazorService?.WireBlazorCdpToAgent();
#endifBuilding and Running
Linux/GTK apps use dotnet run (not dotnet build -t:Run which is MAUI-specific):
# Build and run (in background/async shell)
dotnet run --project <path-to-gtk-project>
# Build only
dotnet build <path-to-gtk-project>Build times are typically fast (~5-10s) since there's no device deployment step.
Network Setup
No special setup needed. Linux apps run directly on localhost — the CLI connects directly to http://localhost:<port>. No port forwarding (unlike Android) or entitlements (unlike Mac Catalyst) required.
Key Simulation
The LinuxAppDriver uses xdotool for key simulation. Install it if needed:
sudo apt install xdotoolFor Wayland-only environments, ydotool may be needed instead. Key simulation is used by the CLI for alert dismissal and keyboard input.
Platform Differences
| Feature | Standard MAUI | Linux/GTK |
|---|---|---|
| NuGet packages | Agent, Blazor | Agent.Gtk, Blazor.Gtk |
| TFM | net10.0-<platform> | net10.0 |
| Build command | dotnet build -f $TFM -t:Run | dotnet run --project <path> |
| Agent startup | Automatic (lifecycle hook) | Manual (app.StartDevFlowAgent()) |
| Network | Varies by platform | Direct localhost |
| Screenshots | VisualDiagnostics | GTK WidgetPaintable → Texture.SaveToPng() |
| Native tap | Platform gesture system | Gtk.Widget.Activate() |
| Key simulation | Platform-specific | xdotool |
| Blazor WebView | WKWebView / WebView2 / Chrome | WebKitGTK 6.0 |
Troubleshooting
Agent Not Starting
1. Ensure app.StartDevFlowAgent() is called after the app is activated 2. Check that Application.Current is available when StartDevFlowAgent() runs 3. Verify the port isn't in use: lsof -i :<port> or ss -tlnp | grep <port>
xdotool Not Working
- On Wayland,
xdotoolmay not work. Tryydotoolinstead - Ensure the app window has focus for key events
WebKitGTK CDP Issues
- WebKitGTK uses
EvaluateJavascriptAsyncfor JS evaluation - The same two-eval CDP pattern (send + poll) applies as other platforms
- Check that
chobitsu.jsis properly loaded in the WebView
macOS (AppKit) Platform Guide
Platform-specific setup and usage for .NET MAUI apps running on macOS via Platform.Maui.MacOS (AppKit).
Table of Contents
- Overview
- Project Structure
- NuGet Packages
- Registration
- Building and Running
- Blazor Hybrid
- Platform Differences
- Troubleshooting
Overview
macOS (AppKit) apps use the community Platform.Maui.MacOS packages to run MAUI on native AppKit (not Mac Catalyst). The TFM is net10.0-macos. Like Linux/GTK, macOS apps typically use a separate app head project rather than adding -macos to the standard MAUI project's TargetFrameworks.
Detect macOS projects:
grep -i 'Platform\.Maui\.MacOS\|net.*-macos' *.csproj Directory.Build.props 2>/dev/nullProject Structure
macOS apps use a separate app head project (similar to Linux/GTK):
src/
├── MyApp/ # Standard MAUI project (iOS, Android, Mac Catalyst, Windows)
├── MyApp.MacOS/ # macOS AppKit app head
│ ├── Program.cs # Entry point: MacOSMauiApplication
│ ├── MauiProgram.cs # macOS-specific builder (UseMauiAppMacOS, AddMacOSEssentials)
│ └── MyApp.MacOS.csproj # References Platform.Maui.MacOS packagesShared source files (pages, view models, services) are typically linked from the main project.
NuGet Packages
The app project needs the Platform.Maui.MacOS packages plus the standard MauiDevFlow packages:
<ItemGroup>
<!-- macOS platform -->
<PackageReference Include="Platform.Maui.MacOS" Version="*" />
<PackageReference Include="Platform.Maui.MacOS.BlazorWebView" Version="*" /> <!-- Blazor only -->
<PackageReference Include="Platform.Maui.Essentials.MacOS" Version="*" />
<!-- MauiDevFlow (standard packages — they include net10.0-macos support) -->
<PackageReference Include="Redth.MauiDevFlow.Agent" Version="*" />
<PackageReference Include="Redth.MauiDevFlow.Blazor" Version="*" /> <!-- Blazor only -->
</ItemGroup>The standard Redth.MauiDevFlow.Agent and Redth.MauiDevFlow.Blazor packages include net10.0-macos targets — no separate macOS-specific MauiDevFlow packages needed.
Registration
Program.cs (Entry Point)
using AppKit;
using Microsoft.Maui.Platform.MacOS;
using ObjCRuntime;
namespace MyApp.MacOS;
[Register("Program")]
public class Program : MacOSMauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
NSApplication.Init();
NSApplication.Main(args);
}
}The [Register] attribute is required.
MauiProgram.cs
using Microsoft.Maui.Platform.MacOS;
using Microsoft.Maui.Platform.MacOS.Controls;
#if DEBUG
using MauiDevFlow.Agent;
using MauiDevFlow.Blazor;
#endif
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiAppMacOS<App>() // NOT UseMauiApp — macOS-specific
.AddMacOSEssentials() // REQUIRED — without this, no window appears
.AddMacOSBlazorWebView() // Blazor only
.ConfigureFonts(fonts => { /* ... */ });
#if DEBUG
builder.AddMauiDevFlowAgent();
builder.AddMauiBlazorDevFlowTools(); // Blazor only
#endif
return builder.Build();
}
}Critical: AddMacOSEssentials() is required — without it the app runs but no window appears.
Building and Running
macOS apps do NOT use -t:Run. Build first, then launch with open:
# Build
dotnet build -f net10.0-macos path/to/MyApp.MacOS
# Find and launch the .app bundle
open path/to/MyApp.MacOS/bin/Debug/net10.0-macos/osx-arm64/AppName.appCode signing: A clean dotnet build produces a valid ad-hoc signature. Do NOT manually re-sign the app — it breaks the signature (SIGKILL on launch). If the app fails to launch, clean rebuild: rm -rf bin obj && dotnet build.
Finding the .app bundle:
find bin/Debug/net10.0-macos -name "*.app" -maxdepth 3Network Setup
No special setup needed. macOS apps run directly on localhost — the CLI connects directly to http://localhost:<port>. No port forwarding or entitlements required.
Blazor Hybrid
Use MacOSBlazorWebView instead of the standard BlazorWebView:
using Microsoft.Maui.Platform.MacOS.Controls;
// In page code-behind, replace BlazorWebView with MacOSBlazorWebView
var blazorWebView = new MacOSBlazorWebView();
blazorWebView.HostPage = "wwwroot/index.html";
blazorWebView.RootComponents.Add(new RootComponent { ... });Chobitsu.js is auto-injected via the Blazor JS module initializer — no manual <script> tag needed.
Platform Differences
| Feature | Mac Catalyst | macOS (AppKit) |
|---|---|---|
| TFM | net10.0-maccatalyst | net10.0-macos |
| Base framework | UIKit via Catalyst | AppKit |
| Packages | Standard MAUI | Platform.Maui.MacOS |
| Project structure | Standard MAUI single-project | Separate app head project |
| Build + Run | dotnet build -t:Run | dotnet build then open App.app |
| Entry point | Standard MAUI | MacOSMauiApplication with [Register] |
| Builder | UseMauiApp<App>() | UseMauiAppMacOS<App>() |
| Essentials | Built-in | AddMacOSEssentials() (required) |
| BlazorWebView | Standard BlazorWebView | MacOSBlazorWebView |
| Entitlements | Required (network.server) | Not needed |
| Native sidebar | N/A | MacOSShell.SetUseNativeSidebar(shell, true) |
Troubleshooting
App Launches But No Window Appears
Cause: Missing AddMacOSEssentials() call in MauiProgram.cs. Fix: Add .AddMacOSEssentials() to the builder chain.
SIGKILL on Launch (Code Signature Invalid)
Cause: Manually re-signing the app bundle or corrupted build artifacts. Fix: Clean rebuild: rm -rf bin obj && dotnet build -f net10.0-macos. Never use codesign manually — the build produces a valid ad-hoc signature.
Blazor Page Shows "Loading..." Indefinitely
Cause: Using standard BlazorWebView instead of MacOSBlazorWebView. Fix: Replace BlazorWebView with MacOSBlazorWebView from Microsoft.Maui.Platform.MacOS.Controls.
No Shell Sidebar Content
Cause: macOS Shell needs explicit native sidebar configuration. Fix: In AppShell code-behind:
FlyoutBehavior = FlyoutBehavior.Locked;
MacOSShell.SetUseNativeSidebar(this, true);Agent Not Connecting
1. Ensure the app launched successfully (window appeared) 2. Check maui devflow list — agent should register within a few seconds 3. If using an older build, clean and rebuild to pick up latest agent code
Setup & Installation
Complete guide for integrating MauiDevFlow into a .NET MAUI app.
Table of Contents
- Install CLI Tools
- Add NuGet Packages
- Register in MauiProgram.cs
- Port Configuration
- Blazor Hybrid Setup
- Mac Catalyst Entitlements
- Android Port Forwarding
- Verify Setup
- Checking for Updates
1. Install CLI Tools
dotnet tool install --global Microsoft.Maui.Cli # maui devflow
dotnet tool install --global androidsdk.tool # android (Android only)
dotnet tool install --global appledev.tools # apple (iOS/Mac only)Verify: maui devflow --version
2. Add NuGet Packages
First, determine whether the project is a standard MAUI app or a Linux/GTK app. There is no -linux TFM — Linux/GTK apps target plain net10.0 (or net9.0) and use the community Maui.Gtk package. Detect this by checking the .csproj:
# Check for GTK indicators in the project
grep -i 'GirCore\|Maui\.Gtk\|Gtk-4\.0' *.csproj Directory.Build.props 2>/dev/nullIf GTK/GirCore references are found, use the Linux/GTK packages below. Otherwise, use the standard MAUI packages.
Standard MAUI Apps (iOS, Android, Mac Catalyst, Windows, macOS)
Add to your MAUI app's .csproj:
<ItemGroup>
<PackageReference Include="Redth.MauiDevFlow.Agent" Version="*" />
<!-- Blazor Hybrid apps also need: -->
<PackageReference Include="Redth.MauiDevFlow.Blazor" Version="*" />
</ItemGroup>Redth.MauiDevFlow.Agent— Required for all MAUI apps (iOS, Android, Mac Catalyst, Windows, macOS AppKit). Provides the in-app agent
for visual tree inspection, screenshots, tapping, filling text, etc.
Redth.MauiDevFlow.Blazor— Required for Blazor Hybrid apps. Provides the CDP bridge
for DOM inspection, JavaScript evaluation, and Blazor debugging.
macOS (AppKit) apps also need the Platform.Maui.MacOS packages — see references/macos.md for the full project setup including entry point, builder configuration, and BlazorWebView differences.
Linux/GTK Apps
Linux/GTK apps (using Maui.Gtk) use separate packages:
<ItemGroup>
<PackageReference Include="Redth.MauiDevFlow.Agent.Gtk" Version="*" />
<!-- Blazor Hybrid apps also need: -->
<PackageReference Include="Redth.MauiDevFlow.Blazor.Gtk" Version="*" />
</ItemGroup>Redth.MauiDevFlow.Agent.Gtk— Agent for Linux/GTK apps. Uses GirCore.Gtk-4.0 for native GTK integration.Redth.MauiDevFlow.Blazor.Gtk— CDP bridge for WebKitGTK-based BlazorWebView on Linux.
3. Register in MauiProgram.cs
using MauiDevFlow.Agent;
using MauiDevFlow.Blazor; // Blazor Hybrid only
var builder = MauiApp.CreateBuilder();
// ... your existing setup ...
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); // Blazor Hybrid only
builder.AddMauiDevFlowAgent();
builder.AddMauiBlazorDevFlowTools(); // Blazor Hybrid only
#endifLinux/GTK Registration
For Linux/GTK apps, use the GTK-specific namespaces and add the agent startup call:
using MauiDevFlow.Agent.Gtk;
using MauiDevFlow.Blazor.Gtk; // Blazor Hybrid only
var builder = MauiApp.CreateBuilder();
// ... your existing setup ...
#if DEBUG
builder.AddMauiDevFlowAgent();
builder.AddMauiBlazorDevFlowTools(); // Blazor Hybrid only
#endifAfter the MAUI app is activated (e.g., in OnActivate or after Application.Current is available):
#if DEBUG
app.StartDevFlowAgent();
// For Blazor, wire CDP to agent:
var blazorService = app.Handler?.MauiContext?.Services.GetService<GtkBlazorWebViewDebugService>();
blazorService?.WireBlazorCdpToAgent();
#endifAgent options:
Port— HTTP port for the agent REST API (default: 9223). Also configurable via.mauidevflowor-p:MauiDevFlowPort=XXXX.Enabled— Enable/disable the agent (default: true)MaxTreeDepth— Max depth for visual tree queries, 0 = unlimited (default: 0)
3b. Port Configuration
Automatic (via broker): The CLI includes a broker daemon that automatically assigns ports to agents. No manual port configuration is needed — the broker handles it. The CLI auto-starts the broker on first use. See the main SKILL.md for details on the broker.
Manual fallback (.mauidevflow): If the broker isn't available, create a .mauidevflow file in the project directory to set an explicit port:
{
"port": 9347
}Both the MSBuild targets and the CLI read this file automatically:
- Build:
dotnet build -t:Run— agent starts on the configured port - CLI:
maui devflow ui status— connects to the configured port (when run from project dir)
Port priority: Explicit --agent-port > Broker discovery > .mauidevflow > default 9223.
How port discovery works: When you run any MAUI or cdp command, the CLI: 1. Auto-starts the broker if not running 2. Queries the broker for agents matching the current project (.csproj in cwd) 3. If one agent matches → uses its port automatically 4. If multiple match → prints a disambiguation table to stderr 5. Falls back to .mauidevflow config file → default 9223
Multiple apps simultaneously: The broker assigns unique ports from range 10223–10899. Use maui devflow list to see all agents, then target a specific one:
maui devflow ui status --agent-port 10224 # target specific agentBlazor options:
Enabled— Enable/disable CDP support (default: true)EnableWebViewInspection— Enable WebView inspection (default: true)EnableLogging— Log debug messages (default: true in DEBUG)
4. Blazor Hybrid: Chobitsu Auto-Injection
No manual setup needed for Blazor Hybrid apps. The Redth.MauiDevFlow.Blazor NuGet package automatically injects chobitsu.js (the CDP implementation) via a Blazor JS initializer. Just add the NuGet package and register in MauiProgram.cs — that's it.
Fallback: Manual Script Tag
If auto-injection doesn't work in your setup (e.g., older .NET versions), add this line before </body> in wwwroot/index.html:
<script src="chobitsu.js"></script>The library detects both approaches — manual script tags take priority over auto-injection.
What if it's not working?
The library checks at runtime and logs a message:
[BlazorDevFlow] ⚠️ No chobitsu script tag found. Auto-injection via JS initializer may not have run.How the file gets there
The chobitsu.js file is included in the NuGet package as a static web asset. It is automatically available at the root of your app's wwwroot/ — no .targets file copying, no manual downloads. It works in both Debug and Release builds (though MauiDevFlow itself should only be referenced in Debug configurations).
5. Mac Catalyst: Entitlements
Mac Catalyst apps need the com.apple.security.network.server entitlement to allow the agent and CDP servers to bind ports. Without this, the app will crash or fail silently.
Option A: Sandbox disabled (simpler for development)
Create or update Platforms/MacCatalyst/Entitlements.plist for Debug builds:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>Option B: Sandbox enabled (required for App Store)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>Reference in your .csproj (Debug only, so Release uses the default entitlements):
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
== 'maccatalyst' and '$(Configuration)' == 'Debug'">
<CodeSignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodeSignEntitlements>
</PropertyGroup>Avoiding TCC permission dialogs: Even with sandbox disabled, macOS prompts for access to ~/Documents, ~/Downloads, ~/Desktop, and dotfiles in ~/ on every rebuild (ad-hoc signing changes the code signature each build). To avoid this, store app data in Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) (~/Library/Application Support/) instead of the home directory root. This path is not TCC-protected.
6. Android: Port Forwarding
After deploying to an Android emulator, set up port forwarding for the broker and agent:
adb reverse tcp:19223 tcp:19223 # Broker (lets agent register with host broker)
adb forward tcp:<port> tcp:<port> # Agent (lets CLI reach agent — get port from `maui devflow list`)The broker reverse (tcp:19223) is needed so the agent inside the emulator can connect to the host's broker daemon. Set this up once per emulator session.
The agent forward uses the port shown in maui devflow list after the agent registers (range 10223–10899).
Fallback (no broker): If using direct mode with a .mauidevflow config file:
adb reverse tcp:9223 tcp:9223 # Direct agent port (single port for Agent + CDP)7. Verify Setup
After building and running the app:
maui devflow list # Should show registered agents (via broker)
maui devflow ui status # Should show agent info, platform, app name
maui devflow webview status # Should show "Connected" (Blazor Hybrid only)If status commands fail:
- Broker not running?
maui devflow broker status— CLI auto-starts the broker, but check if it's healthy - Agent not registered?
maui devflow list— wait a few seconds for the agent to register - Mac Catalyst: Check entitlements (Step 5)
- macOS (AppKit): Ensure
AddMacOSEssentials()is called — see references/macos.md - Android: Check port forwarding (Step 6) — need both
adb reverse tcp:19223andadb forward tcp:<port> - iOS Simulator: Should work without extra config
- Linux/GTK: Should work without extra config — runs directly on localhost
- All platforms: Ensure the app is running and the
#if DEBUGblock is active - Port conflict: Check if another process holds the port:
lsof -i :9223(or your configured port) - Wrong port: Use
maui devflow listto find the assigned port, or ensure CLI is run from the project directory
Quick Checklist
For an AI agent setting up MauiDevFlow in a new project:
1. [ ] Redth.MauiDevFlow.Agent NuGet package added (or Redth.MauiDevFlow.Agent.Gtk for Linux) 2. [ ] Redth.MauiDevFlow.Blazor NuGet package added (Blazor Hybrid only; or Redth.MauiDevFlow.Blazor.Gtk for Linux) 3. [ ] builder.AddMauiDevFlowAgent(...) in MauiProgram.cs inside #if DEBUG 4. [ ] builder.AddMauiBlazorDevFlowTools(...) in MauiProgram.cs (Blazor Hybrid only) 5. [ ] Chobitsu auto-injected via JS initializer (Blazor Hybrid — no manual step needed) 6. [ ] Mac Catalyst entitlements include network.server (Mac Catalyst only) 7. [ ] adb reverse tcp:19223 for broker + adb forward tcp:<port> for agent (Android only) 8. [ ] Linux/GTK: app.StartDevFlowAgent() called after app activation 9. [ ] macOS (AppKit): UseMauiAppMacOS(), AddMacOSEssentials(), MacOSBlazorWebView — see macos.md 10. [ ] Verify with maui devflow list and maui devflow ui status
Checking for Updates
At the start of each session (or periodically), check whether the CLI, skill, and NuGet packages are up to date. Outdated components can cause confusing failures or missing features.
Check CLI version
maui devflow --version
dotnet tool search Redth.MauiDevFlow.CLI | head -5If a newer version is available:
dotnet tool update --global Redth.MauiDevFlow.CLIUpdate the skill
# Check if the skill is up to date (compares local commit SHA against remote)
maui devflow skill-version
# Download the latest skill files from GitHub
maui devflow update-skillThe update-skill command writes a .skill-version file tracking the installed commit SHA. The skill-version command reads this file and compares against the latest remote commit.
AI agents should check at session start: If .claude/skills/maui-ai-debugging/.skill-version exists, run maui devflow skill-version to see if an update is available. If the remote SHA differs from the installed SHA, ask the user if they'd like to update before proceeding.
Check NuGet packages in the project
grep -i 'Redth.MauiDevFlow' *.csproj Directory.Build.props Directory.Packages.props 2>/dev/nullIf packages are outdated:
dotnet add package Redth.MauiDevFlow.Agent
dotnet add package Redth.MauiDevFlow.Blazor # only if Blazor Hybrid
# For Linux/GTK: use .Gtk variants insteadRe-run setup verification
After any updates, walk through the checklist above to ensure everything is still properly configured. A CLI update may introduce new setup requirements.
Troubleshooting
Table of Contents
- Broker Idle Timeout
- Phantom Agent (Connected But Empty Tree)
- Connection Refused
- Build Failures
- CDP Not Connecting
- Mac Catalyst Permission Dialogs
Phantom Agent (Connected But Empty Tree)
Symptom: maui devflow wait returns a port (e.g. 10223) and maui devflow list shows the agent connected, but every inspection command misbehaves:
maui devflow ui treereturns 0 windowsmaui devflow ui statusreports no top-level windowmaui devflow webview statusreports CDP "Not ready"maui devflow logsreturns HTTP 404- Meanwhile
log stream --predicate 'process == "SentenceStudio"'(or equivalent) shows the
app is alive — WebKit activity, layout, network calls all visible in system logs
This is not an auth/code bug — the app is running. The DevFlow agent's HTTP surface is wedged. Most often seen on Mac Catalyst after a kill+relaunch cycle, or on a long-running session where the broker auto-restarted underneath a still-running app.
Recovery (in order — stop as soon as one works):
1. maui devflow diagnose — sometimes prints the actionable cause (broker mismatch, port collision). 2. Force the agent to re-register: kill the app process directly (kill <pid> or close the window), maui devflow list to confirm it disappears, then relaunch via dotnet build -t:Run and maui devflow wait. A fresh launch almost always recovers. 3. Recycle the broker: maui devflow broker stop then maui devflow broker status (which restarts it), then relaunch the app. 4. Last resort: delete ~/.maui/devflow/state.json (or platform equivalent) and restart the broker. This wipes any stale agent registrations.
When to give up and use a different signal: if you've burned ≥10 minutes on this and your goal is to verify behavior (not introspect UI), fall back to indirect verification:
- API logs (Aspire
list_structured_logs) — did the expected request hit the server? - Database queries — did the expected row land in the DB?
- Screenshot via
xcrun simctl io <udid> screenshot out.png(iOS) orscreencapture(Catalyst)
— visual confirmation without DevFlow.
This was the workaround used during the 2026-05 auth-persistence ship: with the agent wedged, zero 401s and zero refresh storms in API logs over the runtime window were accepted as sufficient evidence that the persistence fix held.
Broker Idle Timeout
Symptom: Commands fail with connection errors after returning to debugging after a break.
Cause: The broker daemon shuts down after a period of inactivity (no connected agents, no CLI commands). When you next run a CLI command, the broker auto-restarts, but any previously connected agents are gone — they were registered with the old broker instance.
Fix: 1. Run maui devflow broker status to confirm the broker restarted. 2. Restart the app (re-run dotnet build -t:Run) so the agent re-registers. 3. maui devflow wait to confirm reconnection.
Prevention: For long debugging sessions with breaks, periodically run maui devflow list or maui devflow broker status to keep the broker alive.
Connection Refused / Cannot Connect
If maui devflow ui status fails with connection refused:
1. App not running? Verify the app launched: check the build output for errors. 2. Check the broker: Run maui devflow list to see if the agent registered. If the list is empty, the app may not have connected to the broker yet (wait a few seconds and retry). 3. Wrong port? If using .mauidevflow, ensure the port matches between build and CLI. Run CLI from the project directory so it auto-detects the config file. 4. Port already in use? Another process may hold the port. Check with:
lsof -i :<port> # macOS/LinuxWith the broker, this is less common since ports are auto-assigned. 5. Android? Did you run adb reverse tcp:19223 tcp:19223 (for broker) and adb forward tcp:<port> tcp:<port> (for agent)? Re-run after each deploy. 6. Mac Catalyst? Check entitlements include network.server (see setup.md step 5). 7. macOS (AppKit)? Ensure AddMacOSEssentials() is called and the app window appeared. See references/macos.md for troubleshooting. 8. Linux/GTK? No special network setup needed — runs directly on localhost. Check if the app started successfully. 9. Broker issues? maui devflow broker status to check. maui devflow broker stop then retry (CLI will auto-restart it).
Build Failures
Missing workloads:
error NETSDK1147: To build this project, the following workloads must be installed: maui-iosFix: dotnet workload install maui (installs all MAUI workloads).
SDK version mismatch:
error : The current .NET SDK does not support targeting .NET 10.0Fix: Install the required .NET SDK version, or check global.json for version pins.
Android SDK not found:
error XA0000: Could not find Android SDKFix: Install Android SDK via android sdk install or set $ANDROID_HOME.
iOS provisioning / signing errors: Fix: For simulators, ensure no signing is configured (default). For devices, set up provisioning profiles via apple appstoreconnect profiles list.
General build failure recovery: 1. dotnet clean then retry the build 2. Delete bin/ and obj/ directories: rm -rf bin obj then rebuild 3. Check the full build output (not just the last error) — earlier warnings often reveal the root cause
CDP Not Connecting (Blazor Hybrid)
If maui devflow webview status fails but ui status works:
1. Chobitsu not loading? Check logs for [BlazorDevFlow] messages. If auto-injection failed, add <script src="chobitsu.js"></script> manually to wwwroot/index.html 2. Blazor not initialized? Navigate to a Blazor page first, then retry 3. Check app logs: maui devflow logs --limit 20 — look for [BlazorDevFlow] errors
Mac Catalyst: Repeated Permission Dialogs on Rebuild
If macOS prompts "App would like to access your Documents folder" on every rebuild:
Cause: TCC permissions are tied to the app's code signature. Ad-hoc Debug builds produce a different signature each rebuild → macOS forgets the grant and re-prompts. This happens even with App Sandbox disabled.
Fix: Don't access TCC-protected directories (~/Documents, ~/Downloads, ~/Desktop, or dotfiles like ~/.myapp/ in the home root) programmatically. Instead use:
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)→~/Library/Application Support/(not TCC-protected)NSOpenPanel/NSSavePanelfor user-initiated file access (grants automatic TCC exemption)
If you can't avoid TCC paths, sign Debug builds with a stable Apple Development certificate so the code signature stays consistent across rebuilds.
macOS (AppKit) Issues
For detailed macOS (AppKit) troubleshooting, see references/macos.md.
Common issues:
- No window appears → Missing
AddMacOSEssentials()in builder - SIGKILL on launch → Don't re-sign manually; clean rebuild instead
- Blazor stuck on "Loading..." → Use
MacOSBlazorWebView, not standardBlazorWebView - No sidebar content → Add
MacOSShell.SetUseNativeSidebar(shell, true)+FlyoutBehavior.Locked