Writing / AI Engineering
Can a copy-paste agent beat retrieval?
I gave the same product brief to three AI agents. The screens looked almost identical, but only one approach kept the small behaviours users rely on.
Article
The short version
The first version of this report got part of the answer wrong. It showed that copied components were more reliable than generated ones, but the fix I thought worked did not survive three more runs. This version follows the problem one step further.
The short version
Keep working components in their own files
Three agents built the same five-route product, each with reasoning on and off. Most versions looked finished, but one did not compile. Only the separate-file approach passed all 20 checks in both runs.
Every composition kept as its own component. The page holds a mount point, not the parts.
Everything hand-built. Correct here, but nothing reusable and no library to fall back on.
Read the real documentation, then wrote the components out again anyway.
I wanted to know why an AI-built interface can look finished and still lose basic behaviour. Across 122 runs, one problem kept coming back: a component worked on its own, then broke when the agent connected it to a page.
That joining point is the seam. Compilers, build checks, accessibility scans and screenshots can all miss problems there.
The fix was simple. Keep the working component in its own file and connect the page to it. Do not paste it into the page and give the agent a chance to rewrite it.
What I tested
A component is a reusable part of an interface, such as a button, menu or dialog. The seam is where that reusable part meets the product-specific page around it.
I gave one model the same brief and the same empty project. I changed only how it received the components.
The first agent worked from memory. The second read the real documentation but rewrote every component. The others received working component files, with the final two also receiving a finished example of how the parts should connect.
Five agents, one brief
What each agent is handed before it starts
A component is a reusable interface part — a button, a field, a dialog. Its documentation is the rulebook telling other code how to use it. The only thing that changes between these five is where the component source comes from.
Memory
Memory agent
- Given
- Nothing. An empty project.
- Writes
- The whole interface.
- Component layer
- Invented
Retrieval
Retrieval agent
- Given
- The real component documentation.
- Writes
- The whole interface, components included.
- Component layer
- Rewritten
Copy
Copy agent
- Given
- A tool that copies the real files.
- Writes
- Only the joins between them.
- Component layer
- Copied
Inline
Inline-composition agent
- Given
- The files, plus worked examples pasted into its page.
- Writes
- The joins, around markup it did not place.
- Component layer
- Copied, then inlined
Mounted
Mounted-composition agent
- Given
- The files, plus worked examples as separate components.
- Writes
- The joins, and a prop for each mounted part.
- Component layer
- Copied, kept whole
The final two approaches both started with a correct example. One pasted it inside the page. The other saved it as a separate component file and connected it to the page.
That small difference changed the result.
The test product
This was more than a login screen. The test app had five routes, four charts, ten component families, live table filters, row menus and three dialogs connected to shared data.
I chose a larger product because simple screens can hide structural problems. Shared state, navigation and repeated dialogs force the component layer and the page around it to work together.
The three dialogs were important. They gave me three chances to see whether the same problem would happen again in one build.
With reasoning turned on, the three versions looked almost identical. They also drew the same charts from scratch. A screenshot did not show which build worked best.
Looking right is not the same as working
A successful build is only one kind of success. It proves the code can ship, but it does not prove that the finished interface kept all the behaviour built into its components.
Four different questions
“It works” is four separate claims, and they disagree
Each layer below catches things the one above it cannot see. A build that passes all four is not necessarily good; a build that passes the first two can still be unusable.
- 11 of 6 builds
The compiler
Catches Broken imports, wrong types, invalid property names.
Misses Everything a person would notice using the product.
- 21 of 6 builds
The production build
Catches Whether the project can actually ship.
Misses Whether shipping it would be a good idea.
- 33 of 6 builds
Browser behaviour
Catches Does the menu open, does the filter filter, does state survive a route change.
Misses Anything nobody thought to check for.
- 41 of 6 builds
Composition audit
Catches A reusable part connected in a way that discards behaviour it already had.
Misses Whether the product is any good.
I checked each build in four ways: did the code compile, did the production build pass, did the interface work in a browser, and did the component keep its original behaviour after it was connected to the page?
Only the final check exposed the problem. Most frontend setups do not include it.
The five behaviours that went missing
Most briefs describe the visible task: open the dialog, save the form, update the table. They rarely mention the small behaviours that make the interface feel complete, so I tested those separately.
20 browser checks · 6 builds
The checks that separate them are the ones nobody asked for
Every column is one build of the same product. Every row is one behaviour, driven in a real browser. The five marked rows were never mentioned in the brief — they are behaviours a competent front-end developer would simply expect.
I ran twenty checks on each build. Five covered behaviour that the brief did not spell out, such as closing a menu with Escape or returning keyboard focus to the button that opened a dialog.
On the fifteen checks written in the brief, the memory and separate-file approaches both passed. The difference appeared only in the five behaviours people expect without asking for them.
The same bug appeared three times
One missed behaviour could be random. The same mistake across three dialogs pointed to a repeatable problem in how the agent joined the component to the page.
The defect
The button that opens the dialog is the button focus returns to
A dialog component ships with an opening control attached to it. That attachment is what returns keyboard focus to where the reader was when the dialog closes. Drive the dialog from application state instead, and the attached control starts to look redundant — so it gets deleted. Nothing visible changes.
What the component gives you
<Dialog>
<DialogTrigger>
<Button>New alert rule</Button>
</DialogTrigger>
<DialogContent>…</DialogContent>
</Dialog>Focus returns to the button. Nobody wrote that code.
What the agent wrote instead
<Button onClick={() => setOpen(true)}>New alert rule</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>…</DialogContent>
</Dialog>Identical on screen. Focus now lands on the page body.
app/console/alerts/page.tsxline 118 · driven bydialogOpenapp/console/endpoints/page.tsxline 207 · driven bydialogOpenapp/console/settings/page.tsxline 133 · driven bydeleteOpen
The agent that read the documentation and rebuilt the components made the same mistake in all three dialogs. Each dialog opened, saved and closed, but keyboard focus did not return to the button that opened it.
The cause was easy to miss. The page needed to control when the dialog closed. Once the agent added that control, the component's own opening button looked unnecessary, so it removed it.
The result looked fine. The lost behaviour was invisible.
Why the correct example still failed
I tried giving the agent the correct code in five different ways. In the final test, I pasted it directly into the file the agent was editing.
It deleted it.
Why pasting it in does not work
The agent commits to a design before it ever sees the component
Pasting the composition into the page means the page already contains the agent's own plan for how the dialog opens and closes. Two designs, one file. The edit that follows resolves the conflict in favour of the half the agent wrote itself.
When the page was still a sketch
The composition arrived intact and did not survive the edit.
When the page was already built
The composition arrived into a page that was already built, and survived.
The page already had its own plan for opening and closing the dialog. Pasting in the correct version put two different plans in the same file. The agent resolved that conflict by keeping its own work and deleting the example.
When I pasted the same code into a page that was already complete, it stayed. The timing and location of the code mattered more than the instruction.
The fix: keep the component in its own file
I stopped asking the agent to decide how the finished example should fit inside its page. Instead, I placed the example beside the page and gave the agent a simple connection point.
The fix
Put the component beside the page, not inside it
Exactly the same bytes, copied from exactly the same source. The only change is which file they land in — and whether the page ever sees the component's inner parts.
Inline
Pasted into the page
// app/console/alerts/page.tsx
const [open, setOpen] = useState(false)
…
<Dialog>
<DialogTrigger>…</DialogTrigger>
<DialogContent>…</DialogContent>
</Dialog>The page now holds the agent’s state plan and the component’s inner parts. One of them has to give.
Mounted
Saved as its own component
// app/console/alerts/page.tsx
<NewAlertRuleDialog onCreate={addRule} />
// components/blocks/new-alert-rule-dialog.tsx
// — copied byte for byte, never opened by the agentThe page holds a reference and a prop. The component’s inner parts are somewhere else entirely.
components/blocks/delete-workspace-dialog.tsx69 lines · opening control intactcomponents/blocks/new-alert-rule-dialog.tsx103 lines · opening control intactcomponents/blocks/new-endpoint-dialog.tsx102 lines · opening control intactcomponents/blocks/row-actions-menu.tsx28 lines · opening control intact
I saved the exact same code as a separate component file. The page only received a reference and the data it needed.
Now the page logic and the component logic could not compete inside one file. The agent had no reason to open and rewrite code that already worked.
All four separate components kept their opening controls: three dialogs and one row menu. This held with reasoning both on and off.
Every run was recorded as it happened, so the moment the files arrive is watchable rather than described.
Watch it happen
Four components arriving as their own files
Every run was recorded, so this is the real thing rather than a mock-up: the build being described above, replayed on its own clock at four times speed. The four block files land together, and the agent then opens each one to connect it.
What happened without reasoning
I ran every approach twice: once with reasoning on and once with it off.
Reasoning can improve a model's work, but it also adds time and cost. I wanted to know whether the better result came from extra thinking or from the structure of the files themselves.
With and without reasoning
Without reasoning, rewritten components broke
Reasoning mode lets the model work through a problem before answering. It costs time and money, so it is the first thing teams turn down. Each agent below was run twice, identically, with it on and off.
Memory agent
Retrieval agent
Without reasoning it wrote all 10 components against a superseded version of the library, missing 63 styling hooks.
Mounted-composition agent
Without reasoning, the agent that rebuilt the components used an old version of the library. It missed 63 styling hooks and the project failed with 11 type errors and 26 console errors. It passed only 4 of the 20 checks.
The separate-file approach still passed all 20 checks.
The lesson is simple: reasoning mattered when the agent had to recreate the component. It mattered much less when the agent received working code.
It also cost less
I expected the safer approach to cost more because it read more source code. The bill showed the opposite.
What it costs
More tokens, less money
Reading existing text costs less than writing new text. An agent can use more tokens and still cost less when most of those tokens are input.
The separate-file approach used 3% more tokens but cost 24% less. Reading existing code was cheaper than generating new code.
Token count alone does not tell you the cost. Input and output tokens are priced differently.
Cleaner code, with some cleanup
Passing the interface checks was not the whole story. I also compared the delivered files to the current design-system source and looked for work a developer would need to clean up.
What the code is actually like
The files reveal problems the screens hide
Measured from the delivered project files, not from the model's description of them.
| Build | Components | Library version | Unused files | Page heading |
|---|---|---|---|---|
| Memoryreasoning | none — all bespoke | — | none | yes |
| Memoryno reasoning | none — all bespoke | — | none | yes |
| Retrievalreasoning | 10 written by the agent | current | none | yes |
| Retrievalno reasoning | 10 written by the agent | 10 superseded | none | yes |
| Mountedreasoning | 10 copied | current | 2 files · 122 lines | yes |
| Mountedno reasoning | 10 copied | current | none | yes |
Old component code can look correct in review and still be wrong for the current design system. The separate-file approach matched the library because it used the real files instead of recreating them.
It did leave some unused files behind. They were harmless, but a developer would still need to remove them.
My test setup failed too
Evaluation tools can create the same false confidence as generated interfaces. A clean-looking result means little if the system measuring it has not been checked.
Measuring the measurer
The test rig produced four wrong answers of its own
I found and fixed these four problems while checking the latest results.
| Defect | What happened | What it would have cost |
|---|---|---|
| Screenshots overwrote each other | The short name for a build ignored whether reasoning was on, so the two versions of one agent shared a filename. 18 name collisions across the archive. | Would have replaced half this report's images with the wrong ones. |
| Copied files counted as written | Whole files pasted verbatim from the library were tallied as work the agent had done. | Overstated one agent's hand-written code by up to half. |
| The accessibility count hid findings | Only serious and critical issues were counted. A build reporting zero still had a missing page heading. | A clean number that was not clean. |
| The same logic, copied three times | Rebuilding a project from its recording was implemented separately in three places, and the copies had drifted. | Two of the three could disagree with the real result. |
The system running the tests produced four wrong results of its own. One mixed up screenshots. Another counted copied files as code written by the agent.
I fixed those issues and connected this article to the final data file. If a number changes, the article tests will now flag it.
What designers should do
The practical lesson is not about file organisation alone. It changes what designers need to specify and what teams should review before calling a screen finished.
What to do with this
Four things worth changing on Monday
None of these require knowing how the agent works. They are all things a designer can specify, ask for in review, or check on a build.
Specify the return, not just the frame
A dialog spec that shows the panel is half a spec. Say which control opens it, where focus goes when it closes, and what changed behind it.
Review the join, not the component
The component arrived correct in every build here. What broke was the code connecting it to the product. That is the part with no library behind it.
Ask where the component lives
A composition kept as its own file survived. The same composition pasted into a page did not. It is a structural question you can ask in review.
Check the behaviour nobody wrote down
Escape, focus return, the empty state, the narrow viewport. Every failure that separated these builds was in that category.
Do not treat a component frame as a complete design. For a dialog, say what opens it, where focus goes when it closes, what changes after saving, and what should happen after navigation.
Review the connection between the component and the page, not only the screenshot. A screen can look right, compile and pass an accessibility scan while still losing useful behaviour.
What this test does not prove
The main comparison used one model, one seed for each setup and six builds. The repeated dialog bug gives me confidence that the seam problem is real, but these results are not broad success rates.
One earlier finding did not repeat with reasoning on. The clear result was the failure with reasoning off and the stable performance of the separate-file approach.
I also did not score visual quality, run a screen-reader test or measure what happens after months of product changes. Those are still open questions.
The takeaway
Copying a trusted component can beat generating it, but copying alone is not enough.
Keep stable components whole and in their own files. Let the agent generate the product-specific page around them. Then review the seam between the two, because normal checks may not catch what gets lost there.
Design to code needs a contract, not a better prompt.
AI can write convincing UI with the wrong design system. The reliable path is to retrieve the exact contract, compose only what is needed, and verify the result.

Workflow design: turning templates into leverage.
How a well-organised template library makes recurring work faster, clearer, and easier to improve.

Design reviews as a delivery mechanism.
Treat reviews as a repeatable decision system that creates momentum, clarity, and a visible record of what changes next.
Planning and prioritisation as a two-level design system.
A practical system for connecting product bets with the delivery decisions that keep design work moving.
