I let an agent “fix the empty state on the settings page.” The PR touched seventeen files. Half of them were unrelated tokens. color.gray.400 became color.neutral.400. A shared Button got a new size="sm" default. The empty state itself? Fine. Everything around it? Slightly wrong in a way that took longer to review than writing the change by hand.
That’s the agent UI-diff problem: the model optimizes for “looks done,” not for “touches the smallest surface that owns this UI.”
What unconstrained diffs look like
When I leave the scope fuzzy, I keep getting the same mess:
- Token drive-bys — renames or swaps in the design-token file because the agent “aligned” with something it saw elsewhere
- Shared-component edits — changing Button, Input, or Modal props to make one screen work, then hoping every other screen still looks right
- Layout churn — rewriting a parent flex/grid when a child padding tweak would have been enough
- Style sheet tourism — opening global CSS or a theme file for a one-screen empty state
- Copy edits in the wrong layer — hardcoding strings in the component instead of the i18n key the screen already uses
None of that is malicious. The agent is trying to be helpful. Helpful without boundaries is just blast radius.
The contract I paste into every UI prompt
I stopped saying “fix the empty state.” I paste a short fence:
Touch only files under
app/settings/andcomponents/settings/. Do not edittokens/,theme/, sharedcomponents/ui/, or global CSS. Prefer composing existing primitives. If you need a new prop on a shared component, stop and list the prop — do not add it. Diff should be reviewable in under five minutes.
Then I name the outcome in UI terms:
- Empty settings list shows the illustration + “No integrations yet” + a primary “Connect” button
- Button uses the existing
Buttonprimary variant — no new variants - Spacing uses existing spacing tokens already imported in that folder
- No changes outside the two directories above
If the agent can’t finish inside that fence, I want a question back, not a creative detour through the design system.
Component boundaries as the real unit of work
Design systems already give you the right unit: the component that owns the screen section. Agents ignore it unless you force them.
My mental model:
- Leaf screen work stays in the route/feature folder
- Shared primitives are read-only unless the task is explicitly “extend Button”
- Tokens are read-only unless the task is explicitly a token RFC (and then it needs a changelog, not a drive-by)
So for “settings empty state,” the owner is SettingsEmptyState (or whatever lives next to the list). The agent may create or edit that file and wire it from the page. It may not “improve” Stack spacing while it’s there.
That one rule kills most of the seventeen-file PRs.
How I review agent UI PRs now
Before I read the code, I look at the file list:
- Count directories — more than one feature folder usually means scope creep
-
Scan for
tokens/,theme/,components/ui/— if they’re in the diff and the ticket wasn’t about them, reject - Check shared defaults — any change to default props on a primitive is a hard stop
- Ask for the rollback story — if I revert this PR, does anything else break? If yes, the agent coupled too hard
I also ask the agent (in the PR comment or the next prompt) to paste the list of files it chose not to touch and why. That metacognition sounds silly. It cuts random token edits more than any lint rule I’ve tried.
A reusable prompt ending
This is the footer I keep on UI tasks:
Stay inside the listed paths. Do not rename tokens. Do not change shared component APIs. Do not “cleanup” neighboring files. If the design needs a token or primitive change, reply with a proposal instead of a diff.
Agents are fast at filling a hole in the UI. They’re bad at knowing which walls of the house they’re allowed to move. You have to draw the walls first.
Next time an agent ships a “small UI fix” with a theme file in the diff, send it back with the fence above. The empty state will still get fixed — and your next review will fit in five minutes instead of fifty.
Top comments (0)