Blueprint/Chapter 8
Chapter 8

Building Agentic UIs with Cedar

By Zavier SandersSeptember 21, 2025

Build production-ready AI interfaces with Cedar-OS: state access, chat components, and real-time agent interactions.

Prefer something you can ship today? Start with theQuickstart: Ship One Agent with Mastra— then come back here to deepen the concepts.

Introduction: Why UI Matters for Agents

You've built a powerful AI agent with perfect classification, smart tools, and reliable triggers. But without a good UI, users can't access it effectively.

The gap: Traditional web UIs are built for static content and forms. AI agents need:

  • Real-time streaming responses
  • Tool execution visualization
  • State read/write capabilities
  • Natural language interaction
  • Human-in-the-loop approval flows

Cedar-OS solves this by providing production-ready UI components specifically designed for agentic applications.

Traditional Chat vs Agentic UI

┌─────────────────────────────────────────────────────────────┐
│           TRADITIONAL CHAT UI                               │
│                                                             │
│  User: "Update my tasks"                                   │
│  Bot: "I can't do that, I can only answer questions"       │
│                                                             │
│  ❌ No state access                                        │
│  ❌ No tool execution                                      │
│  ❌ No real-time updates                                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│            AGENTIC UI (Cedar)                               │
│                                                             │
│  User: "Add a task to review the Q4 budget"                │
│  Agent: ✓ Added task "Review Q4 budget" to your list       │
│  [Task appears in your UI instantly]                       │
│                                                             │
│  ✅ Reads current tasks                                    │
│  ✅ Calls addTask tool                                     │
│  ✅ Updates UI state immediately                           │
└─────────────────────────────────────────────────────────────┘

Cedar vs Other Solutions

Why Cedar?

FeatureCedarCopilotKitCustom Build
Owns code✅ Shadcn-style❌ Abstracted✅ Full control
Setup time5 minutes15 minutesHours/days
State access✅ Built-in✅ Built-in🔨 Build yourself
Voice✅ Built-in❌ DIY🔨 Build yourself
Customizable✅ 100%⚠️ Limited✅ 100%
Learning curveLowMediumHigh
Mastra integration✅ First-class⚠️ Requires adapter🔨 Build yourself

Cedar's Philosophy

1. You own the code - All components download Shadcn-style into your project
2. Simple by default - <FloatingCedarChat /> just works
3. Opinionated architecture - Zustand for state, proven patterns
4. Built for Next.js - Optimized for modern React frameworks

When to Use Cedar

Good for:

  • Next.js applications
  • Apps where AI modifies state
  • Voice-first experiences
  • Human-in-the-loop workflows
  • Production-ready UI needed fast

Not ideal for:

  • Non-React frameworks
  • Simple chatbots without state access
  • Heavy customization from scratch preferred

Quick Start

Prerequisites

  • React 18.2.0+
  • Next.js 14+ (recommended)
  • Node.js 18+
  • TypeScript

Installation

# Using Cedar CLI (recommended)
npx cedar-os-cli plant-seed

# Follow prompts:
# - Select "Existing Next.js project"  
# - Choose Mastra template (best for this blueprint)
# - CLI installs dependencies and components

The CLI:

  • Adds Cedar components to your project (Shadcn-style)
  • Installs required dependencies
  • Sets up configuration files
  • Provides starter examples

Environment Setup

# .env.local
# For client-side Cedar components
NEXT_PUBLIC_OPENAI_API_KEY=sk-...

# For server-side (API routes with Mastra)
OPENAI_API_KEY=sk-...

Note: Use NEXT_PUBLIC_OPENAI_API_KEY if Cedar calls AI directly from browser. Use OPENAI_API_KEY for backend routing through Mastra (recommended for production).

Initialize Cedar

// app/layout.tsx
"use client";

import { CedarCopilot } from 'cedar-os';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CedarCopilot
          llmProvider={{
            provider: 'mastra',
            baseURL: 'http://localhost:4111', // Your Mastra backend
            apiKey: process.env.MASTRA_API_KEY, // Optional auth
          }}
        >
          {children}
        </CedarCopilot>
      </body>
    </html>
  );
}

Your First Cedar Component

// app/page.tsx
import { FloatingCedarChat } from 'cedar-os';

export default function Home() {
  return (
    <div className="h-screen p-8">
      <h1>My Agentic App</h1>
      
      {/* That's it! A fully functional AI chat */}
      <FloatingCedarChat />
    </div>
  );
}

What you get:

  • ✅ Floating chat button
  • ✅ Resizable chat panel
  • ✅ Streaming responses
  • ✅ Message history
  • ✅ Tool execution visualization

Chat Component Types

Cedar provides multiple chat layouts for different use cases.

1. FloatingCedarChat

Floating panel that users can open/close.

import { FloatingCedarChat } from 'cedar-os';

export function App() {
  return (
    <FloatingCedarChat
      side="right"              // 'left' | 'right'
      title="AI Assistant"
      collapsedLabel="How can I help?"
      dimensions={{
        width: 400,
        height: 600,
        minWidth: 350,
        minHeight: 400,
      }}
      resizable={true}
      companyLogo={<MyLogo />}
    />
  );
}

Use when: You want persistent access to AI without taking up screen real estate.

2. EmbeddedCedarChat

Inline chat component.

import { EmbeddedCedarChat } from 'cedar-os';

export function DashboardPage() {
  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="col-span-2">
        {/* Your main content */}
      </div>
      
      <div className="col-span-1">
        <EmbeddedCedarChat
          title="Dashboard Assistant"
          height="600px"
          placeholder="Ask about your data..."
        />
      </div>
    </div>
  );
}

Use when: Chat is a primary feature, always visible.

3. Caption-Style Chat

Bottom-anchored chat bar (like code editors).

import { CaptionCedarChat } from 'cedar-os';

export function EditorPage() {
  return (
    <div className="h-screen flex flex-col">
      <div className="flex-1">
        {/* Your editor or content */}
      </div>
      
      <CaptionCedarChat
        position="bottom"
        placeholder="Ask AI to edit..."
      />
    </div>
  );
}

Use when: Building IDE-like or document-centric interfaces.

Comparison

TypeUse CaseScreen UsageVisibility
FloatingGeneral purposeMinimalOn-demand
EmbeddedChat-first appsDedicated spaceAlways visible
CaptionEditors, documentsBottom barAlways visible

State Access Fundamentals

Cedar's killer feature: agents can read and modify your application state safely.

The Problem

// Traditional approach - agent can't access this
const [todos, setTodos] = useState([
  { id: 1, text: 'Learn Cedar', completed: false },
]);

// User: "Mark the first todo as complete"
// Agent: "I can't access your todos" ❌

The Solution: useRegisterState

import { useRegisterState } from 'cedar-os';
import { useState } from 'react';

export function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn Cedar', completed: false },
    { id: 2, text: 'Build amazing AI apps', completed: false },
  ]);

  // Register state so agent can access it
  useRegisterState({
    key: 'todos',
    description: 'A list of todo items that users can check off',
    value: todos,
    setValue: setTodos,
    stateSetters: {
      addTodo: {
        name: 'addTodo',
        description: 'Add a new todo item',
        execute: (currentTodos, args: { text: string }) => {
          const newTodo = {
            id: Date.now(),
            text: args.text,
            completed: false,
          };
          setTodos([...currentTodos, newTodo]);
        },
      },
      toggleTodo: {
        name: 'toggleTodo',
        description: 'Toggle completion status of a todo',
        execute: (currentTodos, args: { id: number }) => {
          setTodos(
            currentTodos.map((todo) =>
              todo.id === args.id
                ? { ...todo, completed: !todo.completed }
                : todo
            )
          );
        },
      },
      removeTodo: {
        name: 'removeTodo',
        description: 'Remove a todo item',
        execute: (currentTodos, args: { id: number }) => {
          setTodos(currentTodos.filter((todo) => todo.id !== args.id));
        },
      },
    },
  });

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => {
              /* handle manually */
            }}
          />
          {todo.text}
        </div>
      ))}
    </div>
  );
}

Now the agent can:

  • Read current todos: "What tasks do I have?"
  • Add todos: "Add a task to review the budget"
  • Toggle todos: "Mark the first task as complete"
  • Remove todos: "Delete the budget task"

How It Works

┌─────────────────────────────────────────────────────────────┐
│              STATE ACCESS FLOW                              │
│                                                             │
│  1. Component registers state                              │
│     useRegisterState({ key: 'todos', ... })                │
│            │                                                │
│            ▼                                                │
│  2. Cedar stores in Zustand                                │
│     Global state accessible to agent                        │
│            │                                                │
│            ▼                                                │
│  3. User asks: "Add a task"                                │
│            │                                                │
│            ▼                                                │
│  4. Agent decides to call 'addTodo'                        │
│            │                                                │
│            ▼                                                │
│  5. Cedar executes stateSetters.addTodo                    │
│     Updates component state                                 │
│            │                                                │
│            ▼                                                │
│  6. React re-renders with new todos                        │
│     UI updates instantly                                    │
└─────────────────────────────────────────────────────────────┘

State Setters: Safe Mutations

State setters are predefined functions the agent can call. This prevents:

  • ❌ Arbitrary state changes
  • ❌ Invalid data
  • ❌ Security issues
// ❌ Without state setters - agent could do anything
setValue({ malicious: 'data' })

// ✅ With state setters - controlled actions
stateSetters.addTodo({ text: 'Valid todo' })

Agent Context & Mentions

Agent Context

Give your agent application-wide context:

import { CedarCopilot } from 'cedar-os';

export default function Layout({ children }) {
  return (
    <CedarCopilot
      llmProvider={{ provider: 'mastra', baseURL: '...' }}
      agentContext={{
        systemPrompt: `You are a helpful assistant for a project management app.
        
Current user: Pro plan customer
Current project: Q4 Product Launch
Team size: 8 people

Help users manage tasks, track progress, and coordinate with the team.`,
        
        additionalContext: {
          userPlan: 'pro',
          projectName: 'Q4 Product Launch',
          teamSize: 8,
        },
      }}
    >
      {children}
    </CedarCopilot>
  );
}

Mentions

Context-aware @mentions for specific states:

import { useMentions } from 'cedar-os';

export function ProjectDashboard() {
  const [project, setProject] = useState({
    name: 'Q4 Launch',
    tasks: [...],
    team: [...],
  });

  // Register as mentionable
  useMentions({
    key: '@project',
    description: 'Current project details',
    value: project,
  });

  return <div>{/* UI */}</div>;
}

User can now:

  • "What's the status of @project?"
  • "Add a task to @project"
  • "Who's on the @project team?"

Connecting to Your Mastra Backend

Backend Configuration

// app/layout.tsx
import { CedarCopilot } from 'cedar-os';

export default function RootLayout({ children }) {
  return (
    <CedarCopilot
      llmProvider={{
        provider: 'mastra',
        baseURL: process.env.NEXT_PUBLIC_MASTRA_URL || 'http://localhost:4111',
        apiKey: process.env.MASTRA_API_KEY, // Optional
        chatPath: '/chat', // Default path for chat endpoints
        voiceRoute: '/chat/voice-execute', // Voice endpoint
      }}
    >
      {children}
    </CedarCopilot>
  );
}

Mastra API Routes

Cedar expects specific endpoints on your Mastra backend:

// Backend: app/api/chat/route.ts (Mastra)
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { todoAgent } from '@/mastra/agents/todo-agent';

export async function POST(request: Request) {
  const { messages, cedarState } = await request.json();

  // Cedar sends registered states in cedarState
  console.log('Available states:', Object.keys(cedarState));

  const result = await streamText({
    model: openai('gpt-4o-mini'),
    messages,
    tools: {
      // Cedar automatically exposes state setters as tools
      ...cedarState.todos?.stateSetters,
    },
  });

  return result.toAIStreamResponse();
}

Streaming Responses

Cedar automatically handles streaming:

// No extra work needed - streaming works out of the box
<FloatingCedarChat />

// Messages stream in real-time
// Tool calls show up as they execute
// State updates happen immediately

Tool Execution Visualization

Cedar shows tool calls in the chat:

User: Add a task to review the budget

🤖 Assistant:
  🔧 Calling addTodo
     { text: "Review the budget" }
  
  ✓ Added task "Review the budget"

Complete Example: AI-Powered Todo App

Let's build a complete todo app where AI can manage tasks.

Architecture

User Interface (React)
    ↓
Cedar Chat Component
    ↓
Registered State (todos)
    ↓
Mastra Backend (API route)
    ↓
AI Agent (decides to call addTodo)
    ↓
State Setter Executes
    ↓
UI Updates Immediately

Implementation

1. Project Structure

app/
├── layout.tsx          # Cedar setup
├── page.tsx            # Todo UI
└── api/
    └── chat/
        └── route.ts    # Mastra backend

2. Layout with Cedar

// app/layout.tsx
"use client";

import { CedarCopilot } from 'cedar-os';
import './globals.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <CedarCopilot
          llmProvider={{
            provider: 'mastra',
            baseURL: '/api', // Use local API route
          }}
          agentContext={{
            systemPrompt: `You are a helpful todo list assistant.

Help users:
- Add new tasks
- Mark tasks complete/incomplete
- Remove tasks
- Organize and prioritize tasks

Be concise and friendly.`,
          }}
        >
          {children}
        </CedarCopilot>
      </body>
    </html>
  );
}

3. Todo UI Component

// app/page.tsx
"use client";

import { useState } from 'react';
import { useRegisterState } from 'cedar-os';
import { FloatingCedarChat } from 'cedar-os';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';

type Todo = {
  id: number;
  text: string;
  completed: boolean;
  priority?: 'low' | 'medium' | 'high';
};

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: 'Learn Cedar-OS', completed: false, priority: 'high' },
    { id: 2, text: 'Build AI app', completed: false, priority: 'medium' },
  ]);

  // Register state with Cedar
  useRegisterState({
    key: 'todos',
    description: 'User's todo list with tasks, completion status, and priorities',
    value: todos,
    setValue: setTodos,
    stateSetters: {
      addTodo: {
        name: 'addTodo',
        description: 'Add a new todo item. Optionally set priority.',
        execute: (
          currentTodos,
          args: { text: string; priority?: 'low' | 'medium' | 'high' }
        ) => {
          const newTodo: Todo = {
            id: Date.now(),
            text: args.text,
            completed: false,
            priority: args.priority || 'medium',
          };
          setTodos([...currentTodos, newTodo]);
        },
      },

      toggleTodo: {
        name: 'toggleTodo',
        description: 'Toggle a todo's completion status',
        execute: (currentTodos, args: { id: number }) => {
          setTodos(
            currentTodos.map((todo) =>
              todo.id === args.id
                ? { ...todo, completed: !todo.completed }
                : todo
            )
          );
        },
      },

      removeTodo: {
        name: 'removeTodo',
        description: 'Remove a todo item by ID',
        execute: (currentTodos, args: { id: number }) => {
          setTodos(currentTodos.filter((todo) => todo.id !== args.id));
        },
      },

      updatePriority: {
        name: 'updatePriority',
        description: 'Change the priority of a todo',
        execute: (
          currentTodos,
          args: { id: number; priority: 'low' | 'medium' | 'high' }
        ) => {
          setTodos(
            currentTodos.map((todo) =>
              todo.id === args.id ? { ...todo, priority: args.priority } : todo
            )
          );
        },
      },

      clearCompleted: {
        name: 'clearCompleted',
        description: 'Remove all completed todos',
        execute: (currentTodos) => {
          setTodos(currentTodos.filter((todo) => !todo.completed));
        },
      },
    },
  });

  const priorityColor = {
    high: 'text-red-600',
    medium: 'text-yellow-600',
    low: 'text-green-600',
  };

  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold mb-8">AI-Powered Todo List</h1>

        <div className="bg-white rounded-lg shadow p-6">
          <div className="space-y-3">
            {todos.length === 0 ? (
              <p className="text-gray-500 text-center py-8">
                No todos yet! Ask the AI to add some.
              </p>
            ) : (
              todos.map((todo) => (
                <div
                  key={todo.id}
                  className="flex items-center gap-3 p-3 hover:bg-gray-50 rounded"
                >
                  <Checkbox
                    checked={todo.completed}
                    onCheckedChange={() => {
                      const setter = useRegisterState.getState().todos
                        ?.stateSetters?.toggleTodo;
                      setter?.execute(todos, { id: todo.id });
                    }}
                  />
                  <span
                    className={`flex-1 ${
                      todo.completed ? 'line-through text-gray-400' : ''
                    }`}
                  >
                    {todo.text}
                  </span>
                  {todo.priority && (
                    <span
                      className={`text-xs font-medium ${
                        priorityColor[todo.priority]
                      }`}
                    >
                      {todo.priority}
                    </span>
                  )}
                </div>
              ))
            )}
          </div>

          <div className="mt-6 pt-6 border-t">
            <div className="flex justify-between text-sm text-gray-600">
              <span>{todos.filter((t) => !t.completed).length} active</span>
              <span>{todos.filter((t) => t.completed).length} completed</span>
            </div>
          </div>
        </div>

        {/* AI Chat Interface */}
        <FloatingCedarChat
          side="right"
          title="Todo Assistant"
          collapsedLabel="Need help with your todos?"
          dimensions={{
            width: 400,
            height: 600,
          }}
          resizable={true}
        />
      </div>
    </div>
  );
}

4. Mastra Backend

// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(request: Request) {
  const { messages, cedarState } = await request.json();

  // Cedar automatically sends registered states
  const todos = cedarState?.todos;

  const result = await streamText({
    model: openai('gpt-4o-mini'),
    messages,
    system: `You are a helpful todo assistant. You can:
    
- View the current todo list
- Add new todos
- Mark todos as complete/incomplete
- Remove todos
- Update priorities
- Clear completed todos

Current todos: ${JSON.stringify(todos?.value || [])}

Be concise and friendly. Confirm actions after completion.`,
    
    tools: {
      // Cedar state setters become tools automatically
      ...(todos?.stateSetters || {}),
    },
  });

  return result.toAIStreamResponse();
}

Testing the App

Try these commands:

User: "Add a high priority task to deploy the app"
Agent: ✓ Added "Deploy the app" with high priority

User: "What tasks do I have?"
Agent: You have 3 tasks:
  1. ✓ Learn Cedar-OS (completed)
  2. Build AI app (medium priority)
  3. Deploy the app (high priority)

User: "Mark the first incomplete task as done"
Agent: ✓ Marked "Build AI app" as complete

User: "Clear all completed tasks"
Agent: ✓ Removed 2 completed tasks

What You Built

✅ Full todo app with AI assistant
✅ Natural language task management
✅ Real-time state updates
✅ Priority system
✅ Complete/incomplete tracking
✅ Production-ready UI

All in ~200 lines of code!


Styling & Customization

Shadcn-Style Components

Cedar components live in your /components directory. You own them completely.

components/
└── cedarComponents/
    ├── FloatingCedarChat.tsx  ← Customize this
    ├── ChatInput.tsx
    ├── ChatBubbles.tsx
    └── ...

Theming

// Customize Cedar theme
<CedarCopilot
  theme={{
    colors: {
      primary: '#3b82f6',
      secondary: '#64748b',
      background: '#ffffff',
      text: '#1e293b',
    },
    borderRadius: '12px',
    fontFamily: 'Inter, sans-serif',
  }}
>
  {children}
</CedarCopilot>

Custom Chat Bubbles

// components/cedarComponents/ChatBubbles.tsx
export function ChatBubbles({ messages }) {
  return (
    <div className="space-y-4">
      {messages.map((msg) => (
        <div
          key={msg.id}
          className={cn(
            'p-4 rounded-lg',
            msg.role === 'user'
              ? 'bg-blue-500 text-white ml-auto'
              : 'bg-gray-100 text-gray-900'
          )}
        >
          {msg.content}
        </div>
      ))}
    </div>
  );
}

Override Internal Functions

import { overrideCedarFunction } from 'cedar-os';

// Custom message sending logic
overrideCedarFunction('sendMessage', async (message, context) => {
  console.log('Custom send logic');
  
  // Add custom processing
  const processed = preprocessMessage(message);
  
  // Call original
  return originalSendMessage(processed, context);
});

Key Takeaways

  1. Cedar solves the UI gap - Agents need more than chat, they need state access
  2. Shadcn-style ownership - You own all the code, customize everything
  3. 5-minute setup - npx cedar-os-cli plant-seed and go
  4. State access is killer - useRegisterState makes agents truly useful
  5. Mastra integration - First-class support for backend agents
  6. Production-ready - Streaming, tool visualization, error handling built-in

What's Next

You now have the fundamentals to build production agentic UIs with Cedar. Continue your journey:

  • Chapter 9-16: Core Patterns - Learn proven agent architectures you'll wire up with these UI components
  • Chapter 20: Advanced Agentic UI Patterns with Cedar - Master advanced Cedar features including:
    • Diff & History - Track and visualize agent changes over time
    • Human-in-the-Loop UI - Build approval workflows and collaborative editing
    • Voice Integration - Add speech-to-text and text-to-speech for multimodal agents
    • Spells - Complex workflow orchestration in the UI
    • Multi-Agent UIs - Coordinate multiple agents in one interface
    • Vision Agent UIs - Image upload, preview, and annotation for vision-powered agents
    • Production Deployment - Scaling, performance optimization, and monitoring

The fundamentals you learned here are the foundation—Chapter 20 shows you how to build sophisticated, production-grade agentic interfaces.

Get chapter updates & code samples

We’ll email diagrams, code snippets, and additions.