
I write a lot of Compose UI with an AI coding agent now. It's fast at producing composables and completely blind to what they actually look like. It writes the code, says it's done, and the first time anyone sees whether the empty state overflows or the error screen actually shows an error is when a human looks at a device. That gap is the entire reason ComposeProof exists. I built it.
It's an MCP server: 51 tools, five shipped waves since February, currently at v1.5.0. 251 npm installs last month. Not a huge number and I'm not going to pretend it is, but it's real, it's growing, and every one of those installs is someone who decided npx composeproof was worth running. I'll take that over vanity metrics any day.
I built it for myself first, on my own time, then put it out publicly. I'm an Android engineer at Equal, and once it actually worked, we started pointing it at real production code there too. People keep asking what "AI-native" is supposed to mean in a dev workflow. This is my answer: not a smarter model, just someone tired enough of a specific bug to go build the tool that catches it.
The bug that made the case for it
Deep-link straight into the app on a cold start, and you'd see "Something went wrong, unable to load conversations." Pull to refresh and it fixed itself immediately. No crash, no ANR, nothing in Crashlytics. Just a wrong screen, once, on one specific path in.
Here's what was actually happening. A deep link skips the normal splash-screen warm-up, so the conversations screen fires its API call before auth has finished hydrating; the token is empty, an interceptor quietly drops the auth header instead of failing loudly, the request comes back 401, and there's nothing cached yet on a cold start, so up comes the generic error state. Nothing throws. Every piece did exactly what it was written to do.
I found it by reading code, tracing the deep-link start-destination override against the normal launch path line by line. Expensive. And the only way a bug like this would ever surface, because it never crashed, so it never shows up anywhere a crash reporter looks. Nobody writes a unit test for "does this screen show the right thing when you arrive from this specific entry path." Manual testing only catches it if someone remembers to walk that exact path on a real device, and remembering competes with whatever's due Friday.
That's the class of bug I built ComposeProof to make cheap to catch.
How I actually use it, day to day
There isn't one mechanism. There are three, and which one I reach for depends entirely on where the bug is hiding.
New composable, no device plugged in yet: that's headless rendering. The agent generates edge-case @Preview functions, empty state, overflow, RTL, whatever applies, and renders every one through Roborazzi at build time. Broken layouts, overflowing text, a list that quietly assumes at least one item exists. All caught before anything touches a screen.
But headless rendering has no idea what your backend actually does under pressure. So I mock the API on a live device instead, zero app code changes. "Make /orders return an empty list." "Make it 500 items." "Make it a 500 error." The agent screenshots what actually rendered for each one. Most of the interesting bugs live here, because almost nobody manually tests what their screen looks like against a malformed API response.
Neither of those two can see inside the running process. Permission grants, the navigation back stack, DataStore values, which coroutines are alive right now, a screenshot is blind to all of it. You can't tell from a screenshot whether a permission is denied or just never asked. An embedded debug SDK, running inside the app itself, can.
The deep-link bug from the top sits exactly at the seam between the last two. A navigation-and-auth timing issue that no screenshot would explain, but that inspecting the running app's actual auth and navigation state would surface fast.
What actually happened in one real session
I pointed the agent at a Compose Multiplatform sticker app and asked it to test drag interactions, no other instructions. It ran preflight to find the connected device, built, installed, launched. It tapped a FAB and the tap failed silently. Instead of stopping there, it read the source code, discovered the FAB actually opens a bottom sheet rather than doing anything directly, and adjusted its plan. Then it tried to drag a sticker with a standard ADB swipe. That doesn't trigger Compose's gesture system at all, so nothing moved. It switched to raw touch events, hit a coordinate mismatch against the actual screen resolution, recalculated, and the drag worked.
Seven tool calls. About 7,300 tokens total, start to finish, including a generated HTML report with screenshots. That's less than the cost of reading one large source file, for a full build-deploy-interact-report loop with zero hand-holding in the middle.
That session is on composeproof.dev as an actual recorded video, unedited. I'd rather you watch it than take my summary of it.
The part that surprised me
I assumed the hard part would be the tooling: get the MCP server right, expose the right primitives, make the screenshots cheap. It wasn't. Give an agent eyes and it still won't use them by default. It writes a composable, renders it once if you're lucky, and calls it done. If something breaks later, it starts guessing fixes instead of looking at what actually broke, which is exactly the failure mode that let the deep-link bug ship in the first place: nobody looked, because looking wasn't the default.
So the tool ships with a skill, not just an API. Render after every change. Read the screenshot critically against a real checklist. Fix based on what you see, not a guess. Render again to confirm the fix actually worked. If you're stuck, stop and ask instead of flailing. That instruction turned out to matter more than any individual tool call, because it's the difference between an agent that has eyes and an agent that actually uses them.
That's the whole bet. Not that AI can write UI code, it already could. That looking at what it wrote can be as automatic as writing it. The deep-link bug took me an afternoon of tracing code by hand. The same class of bug, caught by an agent that renders and checks by default, costs about as much as reading one file. I'll take that trade every time, and 251 people running npx composeproof last month apparently agree.