# Frontend Development Agents

Course: [AI Agents](https://www.devtutorials.ai/learn/ai-agents) · Section: Agents for Development Workflows
Source: https://www.devtutorials.ai/learn/ai-agents/dev-workflows/frontend-agents

> How a frontend agent changes components until the tests pass: state handling, UI debugging, integration tests, and the right tools and stop rule.

Think about building with LEGO from instructions.

A careful builder looks at the picture of the finished model first. They check which pieces they already have, so they don't build a part that's in the box already. After each section, they compare it with the picture.

A careless builder stops as soon as the pieces are all used. They never compare the result with the picture, so they don't notice the wheel is on backwards.

A **frontend development agent** builds the part of the app people see. The "picture of the finished model" is the tests.

**The main idea:** a frontend agent changes components until the tests pass. The most important settings are its tools and its stop rule: "stop when every test passes", not "stop when the code is written".

## A Real-World Example

The design is complete. Now the planning agent's "panel" task is assigned to the frontend agent: update `web/TicketPage.js` so it displays all 4 export statuses.

The tests in `web/TicketPage.test.js` already define the required behavior:

| Status | The page must show |
|---|---|
| queued | `Export queued` |
| running | `Export running` |
| done, 80000 rows | `Done: 80,000 rows` |
| failed | `Export failed <button>Retry</button>` |

At the moment, the component only handles the "done" status and displays `80000` without commas. The frontend agent must fix the component, and it is not allowed to change the tests.

**In simple words:** the tests are the picture on the box. Keep building until the result matches the picture.

## What You Will Learn

In this lesson, you will learn:

1. How a frontend development agent works with a component.
2. How to handle all the possible states of a component.
3. How an agent debugs UI output by comparing the actual result with the expected result.
4. What integration tests are and why they catch more problems.
5. Why the agent's tools and stop condition determine whether the task is truly finished.

The frontend agent has three tools: `read_file`, `write_file`, and `run_tests`. It can modify files in `web/`, but it can never modify a `.test.js` file. This rule prevents the shortcut you saw in Lesson 5.

## Component Work

A **component** is a single part of the user interface, such as the status panel. In this course, a component is a simple function that takes data as input and returns the text that should be displayed.

```js runnable title="A component is a function from data to output"
function renderStatus(exp) {
  return "Done: " + exp.rows + " rows";
}

console.log(renderStatus({ status: "done", rows: 80000 }));
console.log(renderStatus({ status: "queued" }));
```

Expected output:

```text
Done: 80000 rows
Done: undefined rows
```

This is the starting component. It always displays "Done", even when the export is still queued, and it displays `undefined` when there is no row count. Both of these are bugs that users would see.

## State Handling

A component's **state** represents the situation it is currently displaying, such as queued, running, done, or failed. Good **state handling** means the component provides a clear response for every state, including unexpected ones.

A safe approach is to handle each known state separately and finish with a fallback for any unknown state.

```js runnable title="Handle every state, plus a fallback"
function formatRows(n) {
  return n.toLocaleString("en-US") + " rows";
}

function renderStatus(exp) {
  if (exp.status === "queued") return "Export queued";
  if (exp.status === "running") return "Export running";
  if (exp.status === "done") return "Done: " + formatRows(exp.rows);
  if (exp.status === "failed") return "Export failed <button>Retry</button>";
  return "Unknown status";
}

for (const status of ["queued", "running", "done", "failed", "paused"]) {
  console.log(status.padEnd(8) + "-> " + renderStatus({ status, rows: 80000 }));
}
```

Expected output:

```text
queued  -> Export queued
running -> Export running
done    -> Done: 80,000 rows
failed  -> Export failed <button>Retry</button>
paused  -> Unknown status
```

"paused" is not a valid status at the moment. If the backend ever sends it, the page displays "Unknown status" instead of crashing or showing incorrect information.

## UI Debugging

**UI debugging** means finding out why the screen shows the wrong result. An agent uses the same approach as a person: compare what is currently displayed with what is expected, then identify the first difference.

```js runnable title="Find the first difference between wanted and got"
function firstDifference(wanted, got) {
  for (let i = 0; i < Math.max(wanted.length, got.length); i++) {
    if (wanted[i] !== got[i]) return "at character " + i + ": wanted " + JSON.stringify(wanted.slice(i, i + 6)) + ", got " + JSON.stringify(got.slice(i, i + 6));
  }
  return "same";
}

console.log(firstDifference("Done: 80,000 rows", "Done: 80000 rows"));
console.log(firstDifference("Export queued", "Done: undefined rows"));
console.log(firstDifference("Export running", "Export running"));
```

Expected output:

```text
at character 8: wanted ",000 r", got "000 ro"
at character 0: wanted "Export", got "Done: "
same
```

The first difference points directly to the problem. For the "done" status, the comma is missing, so the agent needs to add number formatting. For the "queued" status, the text is incorrect from the very first character, which means that state is not handled at all.

## Integration Tests

A **unit test** checks a single function by itself. An **integration test** checks whether multiple parts work correctly together.

The TicketPage tests are integration tests. `renderStatus` calls `formatRows` from another file, `web/format.js`. If either function breaks, or if they no longer work correctly together, the test fails.

That is why the agent reads `web/format.js` before making changes. A helper function for formatting row counts already exists. Reusing it keeps number formatting in one place, and the tests verify that both parts work correctly together.

```js runnable title="A unit test and an integration test"
function formatRows(n) { return n.toLocaleString("en-US") + " rows"; }
function renderStatus(exp) { return exp.status === "done" ? "Done: " + formatRows(exp.rows) : "Export " + exp.status; }

const check = (name, got, wanted) => console.log((got === wanted ? "PASS " : "FAIL ") + name);
check("unit: formatRows alone", formatRows(80000), "80,000 rows");
check("integration: renderStatus uses formatRows", renderStatus({ status: "done", rows: 80000 }), "Done: 80,000 rows");
check("integration: failed shows Retry", renderStatus({ status: "failed" }), "Export failed <button>Retry</button>");
```

Expected output:

```text
PASS unit: formatRows alone
PASS integration: renderStatus uses formatRows
FAIL integration: failed shows Retry
```

Only one integration test passes, and so does the unit test. However, the Failed state is missing the Retry button. Only a test that checks the complete output can catch that problem.

## Tools and Stop Rule

Two settings determine whether a frontend agent actually finishes the task.

**Tools.** Without `run_tests`, the agent cannot compare the result with the expected behavior. As you saw in Lesson 3, it can write code, but it cannot verify whether the code is correct.

**The stop rule.** "Stop when the code is written" sounds reasonable, but it allows the agent to stop before checking anything. The correct rule is: "stop when all tests pass, or when you are blocked."

```js runnable title="Two stop rules on the same run"
const run = [
  { step: "run_tests", testsPass: false },
  { step: "read TicketPage.js", testsPass: false },
  { step: "write TicketPage.js", testsPass: false, codeWritten: true },
  { step: "run_tests", testsPass: true, codeWritten: true },
];

const weak = (s) => s.codeWritten === true;
const strong = (s) => s.testsPass === true;
console.log("weak rule stops after: " + run.find(weak).step + " (step " + (run.findIndex(weak) + 1) + ")");
console.log("strong rule stops after: " + run.find(strong).step + " (step " + (run.findIndex(strong) + 1) + ")");
```

Expected output:

```text
weak rule stops after: write TicketPage.js (step 3)
strong rule stops after: run_tests (step 4)
```

One step apart, but only one of them knows whether the code works.

The agent starts by running the tests and finds that 4 tests fail. It reads the component, then reads `web/format.js` so it can reuse `formatRows`. Next, it adds support for all 4 states, runs the tests again, and stops only when all 8 tests pass.

Open the **Preview** tab to view the ticket page in a browser-style view, with one card for each status. Enable **Show the files before the agent ran** to compare the original version, where every card displayed "Done" and the row counts had no commas.

This uses the same agent, but the stop rule is "stop when the code is written". The agent writes a shorter solution and reports "Done" at step 5.

Open the **Files** tab and click **Run tests**. The "failed" test still fails because there is no Retry button. The queued and running tests pass only by coincidence because `"Export " + status` happens to produce the expected text.

## Try It with a Real Model

Everything above uses the scripted brain, so every run produces the same result. Real coding agents use a language model, and a model may make different decisions on different runs.

You can try that here:

1. Open **Settings** in the Agent Lab and select **Live model**. The first time you use it, a small model (about 0.9 GB) is downloaded and runs inside your browser. It requires a browser that supports WebGPU.
2. Choose **Frontend agent: export status**, or **Agent, bug A** from Lesson 1 if you want an easier example.
3. Click **Run 5 times**. The model attempts the same task 5 times, using a small amount of randomness in each run.

You'll see results such as "3 of 5 reached the goal". Open **Show trace** on a failed run and look for the first incorrect step, just like you learned in Lesson 25. You may find invalid JSON, an incorrect line number, or a run that stopped before all tests passed.

In live mode, the agent gets two additional tools that real coding agents also use:

- `read_file` shows **line numbers**.
- `replace_lines` can update a small group of lines without rewriting the entire file.

Small models are much better at making small, targeted edits.

The model used here is much smaller than the models used by real coding agents. However, the lesson remains the same at any scale: **the tests and the guardrails determine whether the work is complete, not the model's own claims.**

Some scenarios remain scripted even in live mode. The Agent Lab explains why, for example: "This task is too long for a small in-browser model."

**Easy Way to Remember:** The tests are the **picture on the box**. Look at them before you start and after every change. Stop only when the result matches them.

Never allow a frontend agent to modify test files just to make the tests pass. Block `*.test.js` files in the agent's configuration, as you did in Lesson 5.

## Common Mistakes

| Mistake | Wrong Idea | Correct Idea | What Goes Wrong |
|---|---|---|---|
| Weak stop rule | "Code written means done." | Stop only when all tests pass. | Broken code is reported as finished. |
| No test tool | "The agent can understand the code by reading it." | Give the agent access to `run_tests`. | The agent cannot verify its own work. |
| Missing states | "Done is the main case." | Handle all four states and provide a fallback. | Queued exports display incorrect text such as "Done: undefined rows". |
| Duplicating helper logic | "It's faster to rewrite it." | Reuse `formatRows`. | Different versions of number formatting become inconsistent. |
| Editing tests | "The test is wrong." | Block changes to `*.test.js`. | The tests pass, but the original bug remains. |

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- The tests are the **picture on the box**. Check them before making changes and after every change.
- A component is a function of its state. Handle every known state and provide a fallback for unexpected states.
- Debug UI issues by finding the first difference between the expected output and the actual output.
- Integration tests verify that multiple parts work correctly together.
- Give the agent access to `run_tests`, and stop only when all tests pass.

Next: **Backend and API Agents**, the agent that builds the endpoint the panel reads from.
