DEV Community

Cover image for Think, act, ask: one agent run through Cuelark's 29 agent components
Cuelark
Cuelark

Posted on Edited on

Think, act, ask: one agent run through Cuelark's 29 agent components

The map told you where each piece goes. This one follows a single run, moment by moment, and shows the code that renders each one.


Two days ago we drew the map of an agent interface: seven places every agent app shares, and which component belongs in each. That answered where.

This post answers when, and how. An agent run is a sequence of moments: someone asks, the agent thinks, it acts, it stops to get an approval, it answers, and someone who cares about cost or bugs observes the run afterwards. Cuelark UI's agent kit is 29 React components for shadcn/ui, grouped by exactly those moments.

We'll follow one run through all six, with the real props for every component. The run is fictional (file names, counts and timings are made up for the example), but every API is the one in the registry.

The run: "Invoice totals are a cent off for EU customers. Find out why and fix it."

Setup

One line in components.json:

{
  "registries": {
    "@cuelark": "https://ui.cuelark.com/r/{name}.json"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then install by name, for example npx shadcn@latest add @cuelark/reasoning @cuelark/tool @cuelark/plan. Each component lands in components/ui/ as source you own. The installation guide covers the by-URL route and the shadcn MCP server.

1. Ask

Prompt Input · Queue · Model Selector (and Suggestions)

<PromptInput
  status={running ? "streaming" : "idle"}
  onSubmit={({ text, files }) => send(text, files)}
  onStop={stop}
  toolbar={<ModelSelector models={models} value={model} onValueChange={setModel} />}
/>
Enter fullscreen mode Exit fullscreen mode

status="streaming" is the one prop that matters most: it swaps Send for Stop and binds Esc. The toolbar slot is where Model Selector lives; it's a compact pill that opens a searchable list grouped by provider, with each model's context window.

Thirty seconds in, the user remembers credit notes. The agent is busy, so the follow-up waits:

<Queue items={queued} onItemsChange={setQueued} />
Enter fullscreen mode Exit fullscreen mode

Alt+↑/↓ reorders, × removes, and the agent takes items one at a time.

Which one when: if your agent answers in two seconds, skip Queue. If you only have one model, skip Model Selector. Prompt Input is never optional.

2. Think

Reasoning · Chain of Thought · Task

The model starts thinking. Stream it, then let it get out of the way:

<Reasoning streaming={isThinking}>{reasoningText}</Reasoning>
Enter fullscreen mode Exit fullscreen mode

When streaming flips to false, it folds to "Thought for 6s". Omit duration and it measures itself.

Once there's an approach, named steps read better than raw thought:

<ChainOfThought title="Tracking down the rounding error">
  <ChainOfThoughtStep label="Searched for tax calculation" />
  <ChainOfThoughtStep label="Read src/tax.ts and src/invoice.ts" />
  <ChainOfThoughtStep status="active" label="Checking EU VAT rounding rules" />
  <ChainOfThoughtStep status="pending" label="Propose a fix and run tests" />
</ChainOfThought>
Enter fullscreen mode Exit fullscreen mode

Steps default to "complete", so you only mark the active and pending ones.

Which one when: three components, three shapes of thinking.

  • Reasoning for free text the model produced. You don't control its structure.
  • Chain of Thought for steps your orchestrator names, where order matters.
  • Task for a checklist with a progress bar, where order doesn't: <Task title="Migrate invoices" items={[{ label: "Write helper", status: "done" }]} />.

A common mistake is rendering orchestrator steps inside Reasoning. Then the one thing the user wants to scan is in the faded window they've learned to ignore.

3. Act

Tool · Subagent · Terminal · File Diff · Test Results

The agent searches the code and fetches the VAT guidance:

<Tool name="search_code" status="success" input={{ query: "roundTax" }} output={matches} duration={0.8} />
<Tool name="web_fetch" status="running" input={{ url: vatGuideUrl }} />
Enter fullscreen mode Exit fullscreen mode

status runs "pending" → "running" → "success" or "error". Keep name as the real tool name, not a friendly label. People learn web_fetch in one run, and the real name is what they'll paste into a bug report.

Between calls there's sometimes a beat with nothing to inspect. That's not a Tool; it's a status line:

<TextShimmer>Comparing rounding modes…</TextShimmer>
Enter fullscreen mode Exit fullscreen mode

Text Shimmer is the kit's status line for "doing something right now": CSS-only, and plain muted text under reduced motion.

Then the agent hands the tests to a helper:

<Subagent name="test-runner" task="Run the invoice suite" status="running" activity="Running vitest…">
  <Terminal title="vitest" lines={output} streaming={testsRunning} />
</Subagent>
Enter fullscreen mode Exit fullscreen mode

The sub-agent's own transcript sits on an indented rail inside it. When it's finished, set status="done" and pass duration. Once the run completes, swap the console for structure:

<TestResults
  suites={[{ name: "invoice", tests: [
    { name: "rounds EU totals once", status: "pass" },
    { name: "keeps US line rounding", status: "pass" },
  ] }]}
/>
Enter fullscreen mode Exit fullscreen mode

A suite with a failure opens itself, failing assertion under the test.

And the change itself:

<FileDiff
  filename="src/tax.ts"
  lines={[
    { type: "context", content: "export function roundTax(items: LineItem[]) {" },
    { type: "remove", content: "  return items.map((i) => round(i.tax)).reduce(sum, 0);" },
    { type: "add", content: "  return round(items.map((i) => i.tax).reduce(sum, 0));" },
    { type: "context", content: "}" },
  ]}
  onAccept={apply}
  onReject={discard}
/>
Enter fullscreen mode Exit fullscreen mode

Unchanged runs fold after six lines (foldAfter), and the header records Accept or Reject.

Which one when:

  • Terminal is the live process; Test Results is the verdict. Show Terminal while it runs, Test Results when it's done. Most apps show both, stacked.
  • File Diff is a decision; a plain Code Block is just code. If you want a yes or no, it's a diff.
  • Subagent whenever a different agent did the work, even a short one. Nesting is the only thing that keeps "who did this?" answerable.

4. Approve

Plan · Confirmation · Checkpoint

Before touching code, the agent proposes a plan:

<Plan
  steps={[
    { label: "Round the invoice total once in roundTax" },
    { label: "Call it after summing in invoice.ts" },
    { label: "Run the invoice suite" },
    { label: "Open a draft pull request" },
  ]}
  onApprove={run}
  onReject={cancel}
  autoRun={10}
/>
Enter fullscreen mode Exit fullscreen mode

autoRun={10} drains a ring on Run; the plan starts unless someone cancels. We use it for low-stakes plans in trusted workspaces and leave it off everywhere else. visible (default 4) sets how many steps show before folding; shortcuts={false} frees ⌘Enter and Esc if your app needs them.

Later the agent wants to rewrite stored totals on staging. One action, real side effects:

<Confirmation
  risk="high"
  title="Recalculate 214 staging invoices"
  description="Rewrites stored totals with the new rounding."
  command="pnpm tsx scripts/recalc-invoices.ts --env=staging"
  onApprove={execute}
  onDeny={skip}
/>
Enter fullscreen mode Exit fullscreen mode

risk="high" turns approve into press-and-hold. The command is shown verbatim, so what you approve is what runs.

And before the diff is applied, a way back:

<Checkpoint label="Before rounding fix" time="14:02" onRestore={restore} />
Enter fullscreen mode Exit fullscreen mode

Which one when: Plan approves intent across steps. Confirmation approves one action. Checkpoint isn't an approval at all; it's the undo you place right before anything you might regret. Approving a plan doesn't remove the need for a Checkpoint; they answer different questions. (For reloads, both Plan and Confirmation render past decisions as receipts. We covered that in the map post, so we won't repeat it here.)

5. Answer

Streaming Text · Inline Citation · Sources · Artifact · Branch

The answer streams:

<StreamingText text={answer} streaming={answering} />
Enter fullscreen mode Exit fullscreen mode

New chunks fade up out of a slight blur; text already on screen never moves.

The claim about VAT gets its evidence right next to it:

<p>
  For this invoice type, VAT is rounded on the total, not per line
  <InlineCitation index={1} source={{ title: guideTitle, url: vatGuideUrl, excerpt: quote }} />.
</p>
<Sources sources={[{ title: guideTitle, url: vatGuideUrl }, { title: "src/tax.ts", url: fileUrl }]} />
Enter fullscreen mode Exit fullscreen mode

If the agent also wrote a summary for the finance team, that's too big for a message:

<Artifact kind="Document" title="Rounding fix: summary" onClose={close} onDownload={download}>
  {summary}
</Artifact>
Enter fullscreen mode Exit fullscreen mode

If the user regenerates the answer, keep the old one:

<Branch versions={[firstAnswer, secondAnswer]} onIndexChange={setIndex} />
Enter fullscreen mode Exit fullscreen mode

Which one when:

  • Inline Citation and Sources are a pair, not alternatives. A Sources list on its own makes people guess which source backs which sentence.
  • Artifact for anything people will read full screen or download; the message just points at it.
  • Branch only when there really are versions. Don't show "1 / 1".

6. Observe

Context Meter · Trace Waterfall

This moment isn't for every user. It's for whoever pays the bill or fixes the bug.

<ContextMeter used={84000} max={200000} cost={0.42} breakdown={{ input, output, cached, reasoning }} />
Enter fullscreen mode Exit fullscreen mode

A ring that warms from ink to amber to red as the context fills, with the token breakdown and cost on hover. (Figures above are example values.)

And the whole run on one timeline:

<TraceWaterfall
  spans={[{
    id: "run", name: "fix-eu-rounding", kind: "agent", start: 0, end: 41,
    children: [
      { id: "m1", name: "plan", kind: "llm", start: 0, end: 6, tokens: 1840 },
      { id: "t1", name: "search_code", kind: "tool", start: 6, end: 6.8 },
      { id: "t2", name: "web_fetch", kind: "tool", start: 7, end: 9.5 },
      { id: "sa", name: "test-runner", kind: "agent", start: 22, end: 39,
        children: [{ id: "t3", name: "vitest", kind: "tool", start: 23, end: 38 }] },
    ],
  }]}
/>
Enter fullscreen mode Exit fullscreen mode

kind is "agent", "llm" or "tool". start and end are seconds from the start of the run; leave end off and the span grows live. Set error on a span and it turns red. Hover any bar for duration, tokens and cost. You can see it with real interaction on the Trace Waterfall page.

Which one when: Chain of Thought is the user's view of progress, inside the turn. Trace Waterfall is the developer's view of the whole run, in the run header or a debug panel. If you put a waterfall in a consumer chat thread, you've built a dashboard nobody asked for.

The whole turn, composed

Everything above, as one assistant turn. Data loading is yours; every prop is real.

export function AgentTurn({ run }: { run: Run }) {
  return (
    <Message from="assistant">
      <MessageContent>
        {/* think */}
        <Reasoning streaming={run.thinking}>{run.reasoning}</Reasoning>
        <ChainOfThought title={run.title}>
          {run.steps.map((s) => (
            <ChainOfThoughtStep key={s.label} status={s.status} label={s.label} />
          ))}
        </ChainOfThought>

        {/* act */}
        {run.tools.map((t) => (
          <Tool key={t.id} name={t.name} status={t.status} input={t.input} output={t.output} duration={t.duration} />
        ))}

        {/* approve */}
        <Plan steps={run.plan} onApprove={run.approve} onReject={run.reject} defaultState={run.planDecision} />

        {/* act, delegated */}
        <Subagent name="test-runner" task="Run the invoice suite" status={run.tests.status} duration={run.tests.duration}>
          <TestResults suites={run.tests.suites} />
        </Subagent>

        {/* approve, then act */}
        <Checkpoint label="Before rounding fix" time={run.checkpointAt} onRestore={run.restore} />
        <FileDiff filename="src/tax.ts" lines={run.diff} onAccept={run.apply} onReject={run.discard} />

        {/* answer */}
        <StreamingText text={run.answer} streaming={run.answering} />
        <Sources sources={run.sources} />
      </MessageContent>
    </Message>
  );
}
Enter fullscreen mode Exit fullscreen mode

Outside the thread, the observe moment:

<header className="flex items-center justify-end gap-3">
  <ContextMeter used={run.tokens.used} max={run.tokens.max} cost={run.cost} />
</header>
<TraceWaterfall spans={run.trace} />
Enter fullscreen mode Exit fullscreen mode

Read the comments top to bottom and you get the run: think, act, approve, act, answer. The order of components is the story of the run. If your JSX reads in an order that doesn't match what happened, your users will be confused in the same place.

What this run taught us

  • One moment, one component. The kit has three ways to show thinking because there are three shapes of thinking. Pick by shape, not by looks.
  • Defaults are compact, errors aren't. A failed Tool and a failing test suite open themselves. Everything else waits for a click.
  • Approvals come in three kinds. Intent (Plan), action (Confirmation), and the way back (Checkpoint). Most apps ship only the middle one.
  • Observe lives outside the thread. Context Meter and Trace Waterfall are for the people who pay for or debug the run.

Try it

The fastest way to learn the kit is to take one apart. On the live agent demos, hover any part of the coding, research or data agent and it names itself; click the tag to open its page and copy the code.

Cuelark UI is free and open source (MIT): github.com/cuelark/ui.

FAQ

What are AI agent UI components? Components for the moments of an agent run: asking, thinking, acting, approving, answering and observing. A chat kit covers asking and answering; an agent kit covers the four in between.

How many agent components does Cuelark UI have? 29. By moment: ask (Prompt Input, Queue, Model Selector, Suggestions), think (Reasoning, Chain of Thought, Task, Text Shimmer), act (Tool, Subagent, Terminal, File Diff, Test Results, Stack Trace, Code Block, File Tree, Web Preview), approve (Plan, Confirmation, Checkpoint), answer (Conversation, Message, Streaming Text, Inline Citation, Sources, Artifact, Branch) and observe (Context Meter, Trace Waterfall).

Do they work with the Vercel AI SDK? Yes, or with any stream. They take plain props such as status, input, output, steps and spans.

Are they shadcn/ui components? Yes. They install as source with the shadcn CLI and use your Tailwind CSS v4 theme tokens.

Top comments (0)