React Native macOS apps
For AI agents: this page is Markdown at https://squiggle.sh/docs/macos.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.
Squiggle drives React Native macOS apps as well as simulator apps. The SDK reads your app and taps and types in it, and in Debug builds it also loads a small agent that adds drags, screenshots and the waits that make flows fast, all inside the app, so it can stay in the background while you work.
What works with only the SDK
Add the SDK and most of Squiggle works, with no native code:
- Reading the app
describe,find,queryand the current screen; the component tree, props and render counts; where each element is.- Taps, hover and typing
- Acted on the element directly, with no pointer. A hover runs the element’s
onHoverInoronMouseEnter. - Scrolling
- Moves the list straight to a position, with no gesture and no momentum. It gets the app to a state; it doesn’t test scrolling.
- Assertions and the graph tools
assertandwaitForon the graph,legibility,record,replayandcrawl.- JavaScript performance
- Renders, spans and
perf sql, plus CPU profiles and heap snapshots over Metro.
Without the agent, drags and swipes report unsupported, and there are no animation or settle waits, no network capture or stubs, no log assertions and no screenshots.
Set it up
1 · Add the SDK
Add the SDK as Set up your app describes, without the Expo config plugin: React Native macOS apps aren’t built with expo prebuild. The SDK supports macOS 15 and later, so in macos/Podfile set platform :macos, '15.0', then run pod install in macos/.
squiggle init records your app’s bundle id for each platform it builds for in .squiggle/config.jsonc, as apps. It also sets "platform": "macos" when the app has no ios/ folder.
2 · Build the app in Debug
There is nothing to add for the agent. In a Debug build, the SDK loads it when the app starts, however you start it: from the Dock, Finder or Xcode. A build you sign for distribution, with Developer ID or for the App Store, never loads it, and nothing of the agent is copied into your app.
A Debug build started that way loads the copy of the agent in ~/.squiggle/agent. squiggle init puts it there, and every squiggle up replaces it with the version that came with your squiggle command.
3 · Launch it
squiggle upThere is no device to pick: the bundle id is the whole target, and init recorded it. It launches the app without bringing it to the front, with the agent that came with your squiggle command, and a relaunch waits for the old one to quit. It then prints which agent the app loaded, so you know the agent is in. Over MCP, launch_app does the same.
If your app also has an ios/ folder, add --platform macos: Squiggle takes the macOS app from apps. If init couldn’t read the bundle id, name the app too:
squiggle up com.example.myapp --platform macosWhat the agent adds
The agent works inside the app, not through your mouse and keyboard. The app can be behind other windows, hidden, minimised or on another Space. Your cursor doesn’t move, the app doesn’t come to the front, and macOS asks for no Accessibility permission.
- Press
- Gesture Handler buttons and plain
Pressables. - Drag and pinch
- A drag moves like a finger, so a reorder, a slider or a resize handle responds as it would on a phone. Pinch works the same way.
- Scroll
- Moves the content directly, with no momentum.
- Typing
- Into the field at the point, without taking focus from the app you are using.
- Hover
- At the point, as a real pointer would, so every hover card, tooltip and highlight under it appears, including on views no component of yours owns. Your agent’s
hovertool does it, andleave: truemoves off again. - Screenshots
- Of the app’s own window, including Skia and other Metal content, with no Screen Recording permission.
- Waits and capture
- Animation and settle waits, log assertions, and network capture and stubs when the app is launched with
--capture-net.
Record a video of a run
Add --record to a run, or record: true to your agent’s run_flow:
squiggle flow checkout --recordSquiggle films the app’s main window at its own size and saves an .mp4 in .squiggle/videos, the same as a simulator run, so filmstrip reads it the same way. Only that window is filmed: windows in front of it never appear in the video, and you can keep working while it records.
Filming another app’s window needs the Screen Recording permission for the app that runs squiggle: your terminal, your editor, or Studio. Turn it on in System Settings, under Privacy & Security. Without it the run goes ahead and warns that the recording couldn’t start.
macOS only gives Squiggle new frames while it is drawing the window. While your display sleeps, your screen is locked, or the window is minimised, the video holds the last frame. If the window never drew during the run, there is no video, and the run says “no frames were captured”.
Choose the app your agent drives
Your agent finds macOS apps with no configuration, and drives the one brought up most recently, so you switch apps with squiggle up or launch_app. A running server notices a newly launched app without a restart. When several are running, the target tool lists them and switches by bundle id, or by the word macos; it refuses a name that matches more than one. An app without the agent can still be read and tapped through its SDK. See Connect your agent.
The iOS and macOS builds of one app can run at the same time, each with its own agent. A run aimed at one never connects to the other, and a flow waiting for the app’s own runs to finish only waits for runs on its own device. Pick the platform with --platform on the command line, or target with ios or macos in your agent; the app comes from apps. launch_app refuses a bundle id that is running on both.
Each platform keeps its own results in the one .squiggle folder. Runs, bench history, performance sessions and what your agent learns are recorded with the platform they ran on. A bench run is only measured against earlier runs on the same platform, a learned flow that passes on iOS still counts as unproven on macOS, and perf diff refuses to compare a session with a baseline from the other platform. Product reports read one platform at a time.
Limits
- Release builds are refused
- An app signed for release (with the hardened runtime and without the
get-task-allowentitlement) can’t load the agent, sosquiggle uprefuses it before launching, withE_NO_INJECT_MACOS. Launching without the agent works for any app. - Controls React Native doesn’t own
- A plain AppKit control isn’t driven by React Native, so a tap on one fails.
- No back gesture
- macOS has none, so
backreportsunsupported. Usenavigatewith a route. - Screenshots of a minimised window
- A minimised window, or one resized while covered, has no current picture, so the agent draws the window again itself. That copy leaves out Skia and other Metal content, and masked layers. The reply’s
viafield says which way the image was made. - Frame rate
- The frame rate follows your main display, which may not be the one the window is on.
- No JS frame rate
- On React Native macOS 0.81,
requestAnimationFramecalls back as fast as the JS thread can run instead of once per frame, so Performance shows no JS FPS for a Mac app. Animation loops in your app built onrequestAnimationFramerun that fast too, and keep the JS thread busy. - No mirror in Studio
- Studio lists your running Mac app in its device menu, marked (Mac). Select it and flows, Performance, Product and Analytics follow it, but there is no live mirror: its window is the app’s own.
Measure from outside the app
squiggle probe connects through Metro, so it needs no agent and no rebuild. It counts renders per component (in a development build), counts running animations, including on the Reanimated UI thread, finds setInterval timers fast enough to keep React Native macOS busy, and measures CPU over a fixed window. Point --port at your Metro server. On a macOS target, squiggle doctor also checks your source for those timers. See the CLI reference.