Back to writing

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.

DateAug 20, 2026
Read time11 min read
TagsAI Agents · Evaluation · Design Systems

Can a copy-paste agent beat retrieval?

Can a copy-paste agent beat retrieval?

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.

1
Mounted-composition agent

Every composition kept as its own component. The page holds a mount point, not the parts.

20/20 with reasoning · 20/20 without
2
Memory agent

Everything hand-built. Correct here, but nothing reusable and no library to fall back on.

20/20 with reasoning
3
Retrieval agent

Read the real documentation, then wrote the components out again anyway.

3 broken joins · did not compile without reasoning
The separate-file agent worked with or without reasoning. It also cost less than the agent that rewrote the components.

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 last two differ in one way. One puts the example inside the page. The other keeps it in a separate file.

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.

The analytics console overview built by the memory agent, with stat cards, a bar chart, a line chart and a stacked regional breakdown.

The analytics console overview built by the memory agent, with stat cards, a bar chart, a line chart and a stacked regional breakdown.
The analytics console overview built by the memory agent, with stat cards, a bar chart, a line chart and a stacked regional breakdown.

The same overview built by the retrieval agent, which read the real component documentation and then wrote the components out itself.

The same overview built by the retrieval agent, which read the real component documentation and then wrote the components out itself.
The same overview built by the retrieval agent, which read the real component documentation and then wrote the components out itself.

The same overview built by the mounted-composition agent, which kept every copied composition as its own component file.

The same overview built by the mounted-composition agent, which kept every copied composition as its own component file.
The same overview built by the mounted-composition agent, which kept every copied composition as its own component file.

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.

  1. 1

    The compiler

    Catches Broken imports, wrong types, invalid property names.

    Misses Everything a person would notice using the product.

    1 of 6 builds
  2. 2

    The production build

    Catches Whether the project can actually ship.

    Misses Whether shipping it would be a good idea.

    1 of 6 builds
  3. 3

    Browser behaviour

    Catches Does the menu open, does the filter filter, does state survive a route change.

    Misses Anything nobody thought to check for.

    3 of 6 builds
  4. 4

    Composition audit

    Catches A reusable part connected in a way that discards behaviour it already had.

    Misses Whether the product is any good.

    1 of 6 builds
The layer that separated these six builds is the last one — and it is the only layer that does not exist in a normal front-end toolchain. Nothing in a standard setup is looking at it.

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.

BehaviourMemoryreasoningMemoryno reasoningRetrievalreasoningRetrievalno reasoningMountedreasoningMountedno reasoning
Overview renders shell and kpispasspasspasspasspasspass
Three charts render with real geometrypasspasspasspasspasspass
Overview recent deploys tablepasspasspasspasspasspass
Sidebar navigates to endpointspasspasspassfailpasspass
Search filters the endpoint tablepasspasspassfailpasspass
Row actions menu openspasspasspassfailpasspass
Escape closes row menunot asked forpassfailpasspasspasspass
Add endpoint dialog openspasspasspassfailpasspass
Endpoint cancel closes without addingpasspasspassfailpasspass
Endpoint dialog returns focus to its openernot asked forpassfailfailfailpasspass
Sidebar navigates to alertspasspasspassfailpasspass
Create alert rule adds row and closespasspasspassfailpasspass
Alert dialog returns focus to its openernot asked forpassfailfailfailpasspass
Created alert survives navigationpasspasspassfailpasspass
Reports tabs render with usage chartpasspasspassfailpasspass
Reports tab switch renders costspasspasspassfailpasspass
Settings form renders seededpasspasspassfailpasspass
Workspace name persists across routespassfailpassfailpasspass
Delete dialog opens and escape closes itnot asked forpassfailpassfailpasspass
Delete dialog returns focus to its openernot asked forpasspassfailfailpasspass
Passed20/2015/2017/204/2020/2020/20
Both approaches passed the checks in the brief. The difference appeared in the five behaviours nobody had written down.

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 by dialogOpen
  • app/console/endpoints/page.tsxline 207 · driven by dialogOpen
  • app/console/settings/page.tsxline 133 · driven by deleteOpen
The same deletion happened in 3 separate files in one build, under 2 different state variables. A static analyser and a real browser found it independently.

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.

The retrieval agent's dialog, open. It looks correct, and it is correct, apart from where focus goes when it closes.

The retrieval agent's dialog, open. It looks correct, and it is correct, apart from where focus goes when it closes.
The retrieval agent's dialog, open. It looks correct, and it is correct, apart from where focus goes when it closes.

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 agent writes the page67 linesno opening control
The tool pastes the compositions in279 linesopening control present
The agent edits the result218 linesno opening control

The composition arrived intact and did not survive the edit.

When the page was already built

The agent writes the page176 linesno opening control
The tool pastes the compositions in220 linesopening control present
The agent edits the result239 linesopening control present

The composition arrived into a page that was already built, and survived.

The difference between these two builds is not the tool and not the prompt. It is how much of the page already existed when the composition landed in it.

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 agent

The 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 intact
  • components/blocks/new-alert-rule-dialog.tsx103 lines · opening control intact
  • components/blocks/new-endpoint-dialog.tsx102 lines · opening control intact
  • components/blocks/row-actions-menu.tsx28 lines · opening control intact
All 4 of them kept it — three dialogs and a row menu, in both builds, with and without reasoning. There was no conflict to resolve, because the two designs never shared a file.

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.

The mounted-composition agent's row menu, which opens and closes on Escape without being asked to.

The mounted-composition agent's row menu, which opens and closes on Escape without being asked to.
The mounted-composition agent's row menu, which opens and closes on Escape without being asked to.

Its dialog, whose opening control survived because it was never in the page to delete.

Its dialog, whose opening control survived because it was never in the page to delete.
Its dialog, whose opening control survived because it was never in the page to delete.

The same build with the model's reasoning switched off entirely - visually and behaviourally unchanged.

The same build with the model's reasoning switched off entirely - visually and behaviourally unchanged.
The same build with the model's reasoning switched off entirely - visually and behaviourally unchanged.

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.

Play the full recordingAll six builds · up to 9 minutes each · opens in a new tab
The clip is a short excerpt. The full recording covers all six builds, with a scrubber and the source of every file as it arrived.

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

Reasoning on20/20
Reasoning off15/20

Retrieval agent

Reasoning on17/20
Reasoning offdid not compile

Without reasoning it wrote all 10 components against a superseded version of the library, missing 63 styling hooks.

Mounted-composition agent

Reasoning on20/20
Reasoning off20/20
The separate-file agent passed all 20 checks with reasoning on and off.

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.

The memory agent with reasoning off. It still compiles, still looks right, and quietly stops closing menus on Escape.

The memory agent with reasoning off. It still compiles, still looks right, and quietly stops closing menus on Escape.
The memory agent with reasoning off. It still compiles, still looks right, and quietly stops closing menus on Escape.

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.

Memory agentTokens656,249Cost$0.0181
Retrieval agentTokens1,335,475Cost$0.0349
Mounted-composition agentTokens1,374,779Cost$0.0264
Re-read text Newly read text Text the model wrote
The separate-file agent used more tokens than the retrieval agent but cost less because it wrote less new code.

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.

BuildComponentsLibrary versionUnused filesPage heading
Memoryreasoningnone — all bespokenoneyes
Memoryno reasoningnone — all bespokenoneyes
Retrievalreasoning10 written by the agentcurrentnoneyes
Retrievalno reasoning10 written by the agent10 supersedednoneyes
Mountedreasoning10 copiedcurrent2 files · 122 linesyes
Mountedno reasoning10 copiedcurrentnoneyes
A component written against a superseded version of the library still compiles and still renders. Nothing in a normal review would catch it, because the file looks exactly like the file it should be.

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.

DefectWhat happenedWhat it would have cost
Screenshots overwrote each otherThe 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 writtenWhole 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 findingsOnly 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 timesRebuilding 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.
None caused a crash. Each produced a result that looked believable.

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.

01

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.

02

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.

03

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.

04

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.

The component contract is not the finished experience. A design system can carry behaviour into a product, but product design still owns whether that behaviour survives being connected to anything.

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.