Skip to content

Commit 7a3eaec

Browse files
committed
Add Flutter runtime inspector support
1 parent 1883a5a commit 7a3eaec

29 files changed

Lines changed: 1458 additions & 37 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ packages/inspector-agent/.build/
99
packages/inspector-agent/.swiftpm/
1010
packages/nativescript-inspector/dist/
1111
packages/react-native-inspector/dist/
12+
packages/flutter-inspector/.dart_tool/
13+
packages/flutter-inspector/pubspec.lock
1214
docs/.vitepress/dist/
1315
docs/.vitepress/cache/
1416
cloud/

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
4848
React Native in-app inspector runtime that connects to the Rust server over
4949
WebSocket, publishes React Fiber component hierarchies with Metro source
5050
locations, and performs best-effort debug JS/native prop edits.
51+
- `packages/flutter-inspector/lib/simdeck_flutter_inspector.dart`
52+
Flutter in-app inspector runtime that connects to the Rust server over
53+
WebSocket, publishes widget/render/semantics hierarchies with debug creation
54+
locations, and performs best-effort semantics, focus, text, and scroll actions.
5155

5256
## Working Rules
5357

@@ -56,6 +60,7 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
5660
- Keep browser-only presentation logic in `client/`.
5761
- Keep NativeScript app runtime inspection logic in `packages/nativescript-inspector/`.
5862
- Keep React Native app runtime inspection logic in `packages/react-native-inspector/`.
63+
- Keep Flutter app runtime inspection logic in `packages/flutter-inspector/`.
5964
- Prefer adding a native API endpoint before adding client-only assumptions.
6065
- Do not add a Node or Swift dependency to solve work that already fits in Foundation/AppKit.
6166
- When touching private API usage, keep the adaptation small and explicit and document any simulator/runtime assumptions here.

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ view inside the editor.
3939
- Full simulator control & inspection using private accessibility APIs - available using `simdeck` CLI
4040
- Real-time screen `describe` command using accessibility view tree - available in token-efficient format for agents
4141
- CoreSimulator chrome asset rendering for device bezels
42-
- NativeScript, React Native, UIKit and SwiftUI runtime inspector plugins to view app's view hierarchy live
42+
- NativeScript, React Native, Flutter, UIKit and SwiftUI runtime inspector plugins to view app's view hierarchy live
4343
- `simdeck/test` for fast JS/TS app tests that can query accessibility state and drive simulator controls.
4444
- SimDeck Studio for sharing Simulator streams & automatic PR deployments to on-demand simulators
4545

@@ -182,9 +182,10 @@ booting is unavailable.
182182
`stream` writes an Annex B H.264 elementary stream to stdout for diagnostics or
183183
external tools such as `ffplay`.
184184

185-
`describe` uses the project daemon to prefer React Native, NativeScript, or
186-
UIKit in-app inspectors, then falls back to the built-in private CoreSimulator
187-
accessibility bridge. Use `--format agent` or `--format compact-json` for
185+
`describe` uses the project daemon to prefer React Native, NativeScript,
186+
Flutter, or UIKit in-app inspectors, then falls back to the built-in private
187+
CoreSimulator accessibility bridge. Use `--format agent` or
188+
`--format compact-json` for
188189
lower-token hierarchy dumps. Coordinate commands accept screen coordinates from
189190
the accessibility tree by default; pass `--normalized` to send `0.0..1.0`
190191
coordinates directly.
@@ -241,6 +242,27 @@ so the package can capture React Fiber commits. The auto entrypoint no-ops
241242
outside development, reads `EXPO_PUBLIC_SIMDECK_PORT` when present, and
242243
otherwise scans common SimDeck daemon ports.
243244

245+
## Flutter Inspector
246+
247+
Flutter apps can expose their widget tree, render frames, semantics metadata,
248+
and debug widget creation locations with the Flutter inspector package:
249+
250+
```dart
251+
import 'package:flutter/foundation.dart';
252+
import 'package:flutter/widgets.dart';
253+
import 'package:simdeck_flutter_inspector/simdeck_flutter_inspector.dart';
254+
255+
void main() {
256+
WidgetsFlutterBinding.ensureInitialized();
257+
258+
if (kDebugMode) {
259+
startSimDeckFlutterInspector(port: 4310);
260+
}
261+
262+
runApp(const App());
263+
}
264+
```
265+
244266
## VS Code
245267

246268
Install the `nativescript.simdeck-vscode` extension from the VS Code Marketplace, then

client/src/api/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface AccessibilityNode {
118118
enabled?: boolean | null;
119119
frame?: AccessibilityFrame | null;
120120
frameInScreen?: AccessibilityFrame | null;
121+
flutter?: Record<string, unknown> | null;
121122
help?: string | null;
122123
imageName?: string | null;
123124
inspectorId?: string | null;
@@ -132,11 +133,13 @@ export interface AccessibilityNode {
132133
role?: string | null;
133134
role_description?: string | null;
134135
scroll?: Record<string, unknown> | null;
136+
semantics?: Record<string, unknown> | null;
135137
source?:
136138
| "native-ax"
137139
| "in-app-inspector"
138140
| "nativescript"
139141
| "react-native"
142+
| "flutter"
140143
| "swiftui"
141144
| string
142145
| null;
@@ -161,6 +164,7 @@ export type AccessibilitySource =
161164
| "in-app-inspector"
162165
| "nativescript"
163166
| "react-native"
167+
| "flutter"
164168
| "swiftui";
165169
export type AccessibilitySourcePreference = AccessibilitySource | "auto";
166170

client/src/app/uiState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const TOUCH_OVERLAY_VISIBLE_STORAGE_KEY = "xcw-touch-overlay-visible";
2929
const ACCESSIBILITY_SOURCE_ORDER: AccessibilitySource[] = [
3030
"nativescript",
3131
"react-native",
32+
"flutter",
3233
"swiftui",
3334
"in-app-inspector",
3435
"native-ax",
@@ -153,6 +154,7 @@ export function isAccessibilitySource(
153154
return (
154155
value === "nativescript" ||
155156
value === "react-native" ||
157+
value === "flutter" ||
156158
value === "swiftui" ||
157159
value === "in-app-inspector" ||
158160
value === "native-ax"

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ function NodeDetails({
467467
["Module", node.moduleName ?? ""],
468468
["NativeScript", nativeScriptDescription(node.nativeScript)],
469469
["React Native", reactNativeDescription(node.reactNative)],
470+
["Flutter", flutterDescription(node.flutter)],
470471
["UIKit Class", node.className ?? ""],
471472
["Last JS", lastUIKitScriptText(node)],
472473
["Value", node.AXValue ?? ""],
@@ -727,6 +728,7 @@ function errorMessage(error: unknown): string {
727728
const HIERARCHY_SOURCE_ORDER: AccessibilitySource[] = [
728729
"nativescript",
729730
"react-native",
731+
"flutter",
730732
"swiftui",
731733
"in-app-inspector",
732734
"native-ax",
@@ -756,6 +758,9 @@ function sourceLabel(source: AccessibilitySource): string {
756758
if (source === "react-native") {
757759
return "React Native";
758760
}
761+
if (source === "flutter") {
762+
return "Flutter";
763+
}
759764
if (source === "swiftui") {
760765
return "SwiftUI";
761766
}
@@ -795,6 +800,17 @@ function reactNativeDescription(
795800
return [tag, testID, nativeID].filter(Boolean).join(" / ");
796801
}
797802

803+
function flutterDescription(value: Record<string, unknown> | null | undefined) {
804+
if (!value) {
805+
return "";
806+
}
807+
const widgetType =
808+
typeof value.widgetType === "string" ? value.widgetType : "";
809+
const stateType = typeof value.stateType === "string" ? value.stateType : "";
810+
const key = typeof value.key === "string" ? value.key : "";
811+
return [widgetType, stateType, key].filter(Boolean).join(" / ");
812+
}
813+
798814
function lastUIKitScriptText(node: AccessibilityNode): string {
799815
const direct = stringRecordValue(node.uikitScript, "script");
800816
if (direct) {

client/src/styles/components.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,12 @@
624624
color: color-mix(in srgb, #61dafb 78%, var(--text));
625625
}
626626

627+
.hierarchy-source-pill.source-flutter {
628+
border-color: color-mix(in srgb, #54c5f8 50%, var(--border));
629+
background: color-mix(in srgb, #54c5f8 14%, transparent);
630+
color: color-mix(in srgb, #54c5f8 80%, var(--text));
631+
}
632+
627633
.hierarchy-source-pill.source-swiftui {
628634
border-color: color-mix(in srgb, #ff6b9d 50%, var(--border));
629635
background: color-mix(in srgb, #ff6b9d 14%, transparent);

docs/.vitepress/config.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ export default defineConfig({
135135
text: "React Native Runtime",
136136
link: "/inspector/react-native",
137137
},
138+
{
139+
text: "Flutter Runtime",
140+
link: "/inspector/flutter",
141+
},
138142
],
139143
},
140144
],

docs/api/inspector-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Params:
122122
{ "includeHidden": false, "maxDepth": 20, "source": "uikit" }
123123
```
124124

125-
By default the agent returns the published framework hierarchy (e.g. NativeScript) when one exists. Pass `"source": "uikit"` to force the raw UIKit tree.
125+
By default the agent returns the published framework hierarchy (for example NativeScript, React Native, Flutter, or SwiftUI) when one exists. Pass `"source": "uikit"` to force the raw UIKit tree when the runtime supports UIKit inspection.
126126

127127
Published framework nodes may include `sourceLocation`:
128128

docs/api/rest.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ Returns the current accessibility tree. The server merges framework inspectors,
442442
| `auto` _(default)_ / unset | Use the most accurate source available, falling back to AX. |
443443
| `nativescript` / `ns` | Force the NativeScript logical tree if a NativeScript inspector is connected for the foreground app. |
444444
| `react-native` / `rn` | Force the React Native component tree if a React Native inspector is connected for the foreground app. |
445+
| `flutter` / `fl` | Force the Flutter widget tree if a Flutter inspector is connected for the foreground app. |
445446
| `swiftui` / `swift-ui` | Force the published SwiftUI logical tree if the Swift agent root publisher is installed in the app. |
446447
| `uikit` / `in-app-inspector` | Force the raw UIKit hierarchy from the in-app inspector agent (NativeScript or Swift). |
447448
| `native-ax` / `ax` | Always use the native accessibility snapshot. |
@@ -456,8 +457,8 @@ The response always includes:
456457
```json
457458
{
458459
"roots": [...],
459-
"source": "nativescript|react-native|swiftui|in-app-inspector|native-ax",
460-
"availableSources": ["nativescript", "react-native", "swiftui", "in-app-inspector", "native-ax"],
460+
"source": "nativescript|react-native|flutter|swiftui|in-app-inspector|native-ax",
461+
"availableSources": ["nativescript", "react-native", "flutter", "swiftui", "in-app-inspector", "native-ax"],
461462
"fallbackReason": "...",
462463
"inspector": { ... }
463464
}

0 commit comments

Comments
 (0)