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?
Feature | Cedar | CopilotKit | Custom Build |
---|---|---|---|
Owns code | ✅ Shadcn-style | ❌ Abstracted | ✅ Full control |
Setup time | 5 minutes | 15 minutes | Hours/days |
State access | ✅ Built-in | ✅ Built-in | 🔨 Build yourself |
Voice | ✅ Built-in | ❌ DIY | 🔨 Build yourself |
Customizable | ✅ 100% | ⚠️ Limited | ✅ 100% |
Learning curve | Low | Medium | High |
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
Type | Use Case | Screen Usage | Visibility |
---|---|---|---|
Floating | General purpose | Minimal | On-demand |
Embedded | Chat-first apps | Dedicated space | Always visible |
Caption | Editors, documents | Bottom bar | Always 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
- Cedar solves the UI gap - Agents need more than chat, they need state access
- Shadcn-style ownership - You own all the code, customize everything
- 5-minute setup -
npx cedar-os-cli plant-seed
and go - State access is killer -
useRegisterState
makes agents truly useful - Mastra integration - First-class support for backend agents
- 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.