DEV Community

Ravi
Ravi

Posted on

Day 2: Generative UI Gen 1 — Static Components with AG-UI

This is Day 2 of my 6-part series on how LLMs rewrote the user interface over the past year. Day 0 covered why chat hit its limits; Day 1 covered the vibe coding toolchain. Today we get to the real shift: agents that render UI at runtime.

The control–freedom axis

Every generative UI architecture answers one question: how much do you trust the agent with your interface?

Gen 1 — static generative UI — gives the safest answer: not much. The agent never produces UI code. It picks from components you built, and fills them with data. Your design system stays intact, your accessibility work survives, and the worst an LLM hallucination can do is put weird data in a well-behaved component.

This is the pattern AG-UI standardizes, and it's where most production teams should start.

What AG-UI actually is

AG-UI (Agent–User Interaction protocol, from the CopilotKit team) is to agent-frontend communication what MCP is to agent-tool communication. The mental model:

  • MCP connects agents to tools
  • A2A connects agents to each other
  • AG-UI connects agents to users

It's a bidirectional event stream (typically SSE) between any agentic backend and your frontend. The agent emits typed events — messages, tool calls, state deltas, lifecycle signals — and your UI reacts. It's already adopted across LangChain, Microsoft Agent Framework, AWS, Mastra, and PydanticAI, which matters: you're not betting on a framework, you're betting on a wire protocol.

Building it: a flight search agent

Let's make the flight example from Day 0 real. The frontend registers a renderable action — a component the agent is allowed to invoke:

"use client"
import { useCopilotAction } from "@copilotkit/react-core"

export function FlightSearch() {
  useCopilotAction({
    name: "showFlights",
    description:
      "Display flight options to the user as interactive cards",
    parameters: [
      {
        name: "flights",
        type: "object[]",
        attributes: [
          { name: "airline", type: "string" },
          { name: "flightNumber", type: "string" },
          { name: "departure", type: "string" },
          { name: "price", type: "number" },
        ],
      },
    ],
    render: ({ args, status }) => (
      <FlightCardList
        flights={args.flights}
        loading={status !== "complete"}
        onSelect={(f) => bookFlight(f)}
      />
    ),
  })

  return <CopilotChat />
}
Enter fullscreen mode Exit fullscreen mode

That's the entire trick. showFlights becomes a tool in the agent's toolbox — except instead of hitting an API, "calling" it renders your FlightCardList component inline in the conversation, streaming in as the arguments arrive (status tells you when they're complete).

The backend doesn't know or care about React. A LangGraph agent just calls the tool:

from langchain_core.tools import tool

@tool
def show_flights(flights: list[dict]) -> str:
    """Display flight options to the user as interactive cards."""
    # No implementation needed — the frontend IS the implementation.
    return f"Displayed {len(flights)} flights to the user."

agent = create_react_agent(
    model=ChatAnthropic(model="claude-sonnet-4-5"),
    tools=[search_flights_api, show_flights],
)
Enter fullscreen mode Exit fullscreen mode

The user asks "flights to Austin Friday morning" and gets sortable cards with a book button — not a paragraph. Same model, radically better interface, and the agent never wrote a line of UI.

Streaming state, not just components

The second AG-UI capability that changes UX: shared state. Long-running agents emit STATE_DELTA events as they work, and the frontend subscribes:

import { useCoAgent } from "@copilotkit/react-core"

function ResearchProgress() {
  const { state } = useCoAgent<ResearchState>({
    name: "research_agent",
  })

  return (
    <ProgressPanel
      completed={state.sources_reviewed}
      total={state.sources_found}
      currentStep={state.current_step}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Remember the Day 0 complaint that agent work scrolls past as an unreadable transcript? This is the fix: the transcript stays for conversation, while a live panel — progress bars, step lists, partial results — reflects what the agent is actually doing. Random access instead of append-only.

Human-in-the-loop, done properly

Gen 1 also fixes the scariest Day 0 problem: approving agent actions by skimming a wall of text. With renderAndWaitForResponse, the agent pauses until the user acts on a real component:

useCopilotAction({
  name: "confirmBooking",
  renderAndWaitForResponse: ({ args, respond }) => (
    <BookingConfirmCard
      flight={args.flight}
      total={args.totalPrice}
      onApprove={() => respond?.("approved")}
      onReject={() => respond?.("rejected")}
    />
  ),
})
Enter fullscreen mode Exit fullscreen mode

A purchase approval is now a card with the price in large type and two buttons — not "the user said OK somewhere in the chat." When real money or destructive operations are involved, this pattern alone justifies Gen 1.

The limits (a.k.a. why Gen 2 exists)

Static generative UI has a ceiling, and you hit it fast:

  1. You must predict every component in advance. The agent can only show what you've built. Novel situations degrade back to text.
  2. Cross-platform means re-implementing. Your FlightCardList is React. The mobile team rebuilds it. The Slack integration can't use it at all.
  3. Combinatorics bite. A travel agent might need flights × hotels × budgets × comparisons × maps. Building and maintaining every variant is a frontend team's full-time job — which was the thing we were trying to escape.

The obvious next question: what if the agent could compose UI from primitives — cards, lists, forms — without you pre-building every screen, and without handing it raw code execution? That's declarative generative UI, Google's A2UI, and it's tomorrow's post.

What's next

  • Day 3: Generative UI Gen 2 — declarative specs with A2UI
  • Day 4: Generative UI Gen 3 — MCP Apps and open-ended surfaces
  • Day 5: Beyond chat — canvas interfaces, adaptive UX, and the security bill coming due

If you want to play with Gen 1 today: npx copilotkit@latest init gets you a working AG-UI setup in about five minutes. See you tomorrow.

Top comments (0)