Skip to main content

guides/agent-pitfalls.md

# Agent pitfalls (Dialup)

This guide records **common misunderstandings** coding agents make when working on Dialup apps.
Add a new section here whenever a review or production issue reveals a pattern agents get wrong.

---

## 1. `layout.css` classes already apply on every page

### The mistake

An agent sees page-specific CSS such as `agent_demo/page.css` and assumes that **each page only has access to its own `page.css`**. It then reimplements shared UI (buttons, code blocks, shadows) with new selectors like `.mcp-handoff-btn` or `.mcp-add button`.

### The reality

Dialup **colocation CSS** works like this:

1. `layout.css` (next to `layout.ex`) defines styles shared by all pages under that layout.
2. `page.css` (next to `page.ex`) adds **page-specific** rules only.
3. At render time, the framework injects **both** into one `<style data-dialup-css>` block.

DOM structure:

```html
<div class="d-layout">      <!-- layout.css scope -->
  <nav>...</nav>
  <main>
    <div class="d-page">  <!-- page.css scope -->
      ... page content ...
    </div>
  </main>
</div>
```

Layout wraps the page. Classes from `layout.css` — for example `.btn`, `.btn-primary`, `.btn-ghost` — match elements **inside any page** as long as you put the class on the element.

There is **no separate import step** and **no UI component module** required. Reuse is class-based:

```heex
<.dialup_action navigate="/docs" class="btn btn-ghost">Get Started</.dialup_action>
<button type="submit" class="btn btn-ghost">Submit</button>
<button type="button" class="btn btn-ghost btn-block" data-mcp="handoff">Hand off</button>
```

### What belongs where

| File | Use for |
|------|---------|
| `layout.css` | Site-wide tokens, buttons (`.btn*`), nav, docs chrome, shared utilities (`.btn-block`, `.btn-sm`) |
| `page.css` | Layout and content **unique to that page** (grids, cards, domain-specific regions) — not another copy of button styles |

### How to avoid it

Before adding button CSS to `page.css`, check `layout.css` for existing `.btn` variants. Prefer:

- `.btn.btn-ghost` — purple CTA
- `.btn.btn-primary` — light background
- `.btn.btn-accent` — accent2 highlight
- `.btn.btn-sm` — compact control
- `.btn.btn-block` — full width

Add a **new variant in `layout.css`** only when the design system genuinely needs one, not per page.

### Reference implementation

See `site/lib/app/layout.css` (definitions) and `site/lib/app/agent_demo/page.ex` (usage on MCP demo buttons).

---

## 2. `ws-change` inputs should not return `{:update, assigns}`

### The mistake

A text field uses `ws-change` + `ws-debounce`, but `handle_event/3` returns `{:update, assigns}`. Every debounced keystroke triggers a full `#dialup-root` morph. The input has `value={@field}` from the server, so the cursor jumps and characters disappear — especially on deployed sites with higher WebSocket latency.

### The reality

For live typing, follow [Events — ws-change](./events.md):

- Human input over `ws-change` → `{:noreply, assigns}` (state only, no DOM replace)
- Optional feedback elsewhere → `{:patch, id, html, assigns}` on a **different** element (see `/demo` `draft_change`)
- Agent-only updates that must refresh the field → `{:patch, input_id, render_input(assigns), assigns}`

### Reference implementation

`site/lib/app/agent_demo/page.ex` — `set_project/3`  
`site/lib/app/demo/page.ex` — `draft_change/3`