Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ui-agent-loop

Point a coding agent at a UI screenshot. Get back a pixel-perfect HTML/CSS replica.

target, render and diff side by side

The idea

LLMs write plausible CSS from a screenshot but can't tell whether the result actually looks right. This repo fixes that with the simplest pattern in agent engineering: give the model an objective signal and let it iterate until that signal says "done".

It's a direct implementation of the Ralph loop for UI replication:

target.png ──> agent edits index.html ──> render via headless Chromium
                      ▲                          │
                      └──── feedback.md ◄── pixel diff + element blame
                                                 │
                                          match ≥ 99.5%? ──> done

The agent is replaceable. The harness provides what any agent needs to converge: an objective score it cannot argue with, and feedback specific enough to act on.

A loop run

A verbatim transcript. The agent here is a scripted stand-in that applies three progressively better drafts — the loop only sees "a CLI that edits index.html", so it runs without an LLM (see Works with any agent):

$ python loop.py examples/pricing-card/target.png --agent "sh ../demo-agent/agent.sh" --run-dir runs/demo

target    examples/pricing-card/target.png  (800x560)
run dir   runs/demo
agent     sh ../demo-agent/agent.sh
done at   99.5% pixel match, max 8 iterations

--- iteration 1/8 ----------------------------------------
applied draft 1
  match 93.78%  (27,883 px off, agent 0s, eval 2.1s)  (new best)

--- iteration 2/8 ----------------------------------------
applied draft 2
  match 98.77%  (5,528 px off, agent 0s, eval 1.1s)  (new best)

--- iteration 3/8 ----------------------------------------
applied draft 3
  match 100.00%  (0 px off, agent 0s, eval 0.7s)  (new best)

converged: 100.00% >= 99.50% after 3 iteration(s)

result   runs/demo/index.html
compare  runs/demo/feedback/side_by_side.png

Each iteration, the harness renders index.html in headless Chromium, pixel-diffs it against the target, translates the diff into actionable feedback, and hands that to a fresh agent run. The loop stops when the render matches the target, not when the agent thinks it's done.

What the feedback looks like

A raw score of 93.8% gives an agent nothing to act on. The evaluator attributes mismatched pixels to the DOM elements that own them, reports their computed styles, and flags target content that has no counterpart in the render at all. From feedback/feedback.md:

match: **93.78%** — 27,883 of 448,000 pixels differ (viewport 800x560, anti-aliasing ignored)
status: NOT CONVERGED (needs >= 99.50%)

## Element blame — your rendered elements, worst first

1. `div.card`
   box (240,104) 320x353 · 14% of its own box mismatched · 59% of all mismatch
   bg #ffffff · color #000000 · font 16px/400 · pad 28px · radius 12px
2. `button.cta` — "Start free trial"
   box (268,388) 264x41 · 93% of its own box mismatched · 36% of all mismatch
   bg #6d28d9 · color #ffffff · font 15px/600 · pad 12px 0px · radius 8px
...
unattributed: 41% of the mismatch lies under none of your elements — content from the target is missing or misplaced.

The same mismatch as the evaluator draws it (feedback/diff.png — red over a faded target):

mismatch heatmap

Wrong purple, narrower card, missing text. The feedback names the guilty elements with their computed styles, and the unattributed line flags missing content. Alongside it: diff.png with red highlighted mismatch and the composite side_by_side.png.

Quickstart

git clone https://github.com/Marvin0212/ui-agent-loop && cd ui-agent-loop
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
python make_targets.py    # re-render targets with your local fonts

python loop.py examples/pricing-card/target.png

Or bring your own screenshot:

python loop.py ~/Desktop/some-ui.png --iters 10 --threshold 0.99

Runs are saved under runs/ with full iteration history. Interrupt anytime, resume with --run-dir.

Works with any agent

An "agent" is anything that reads a task from stdin and edits files in its cwd:

agent --agent
Claude Code claude -p --dangerously-skip-permissions
Cursor CLI cursor-agent -p
OpenAI Codex CLI codex exec --full-auto -
smoke test cp ../../examples/pricing-card/reference.html index.html

The smoke test copies the ground-truth HTML and converges at 100% in one iteration: a free end-to-end test of the render/diff/blame pipeline at zero token cost.

Examples

Three targets of increasing complexity, each with the reference.html used to render it. The agent never sees this reference file:

pricing card login dashboard
pricing-card 800x560 login 1080x700 dashboard 1280x800

How it works

~500 lines of Python across two files.

loop.py runs the outer loop in about 160 lines. No planner, no orchestration graph, no conversation memory. All state lives in files in the run directory, which makes every iteration inspectable and any run resumable.

evaluate.py is the evaluator of around 350 lines:

  1. Render: Playwright at the target's viewport, animations and text caret disabled for deterministic pixels.
  2. Diff: pixelmatch with anti-aliasing detection so font edges don't poison the score.
  3. Blame: intersects the mismatch mask with DOM bounding boxes, attributing bad pixels to their owners. Unaccounted-for mismatch is flagged as missing content.
  4. Verdict: one number is the convergence criterion, the loop's stop condition, and the exit code.

Repo layout

loop.py          outer loop: run agent -> evaluate -> feedback -> repeat
evaluate.py      render via Playwright -> diff via pixelmatch -> blame -> feedback.md
PROMPT.md        task contract every agent iteration receives
make_targets.py  re-render examples/*/reference.html -> target.png locally
examples/        three targets: pricing-card, login, dashboard
runs/            one workspace per run, gitignored

Limitations

  • Fonts are machine-specific. System fonts render differently across OSes. make_targets.py regenerates targets locally for honest scores.
  • Static, single viewport. One resolution, no hover states, no interactivity.
  • Pixel-perfect != good HTML. The metric measures visual fidelity, not code quality.

License

MIT

About

Agent harness that replicates UI screenshots as pixel-perfect HTML — headless render, pixel diff, per-element blame as the feedback loop

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages