- ThemesPage: Custom dropdown with theme cards for mobile - ThemesPage: Collapsible variables section - ThemePreview: Responsive grid and compact layout - VariableEditor: Separate radius preview with border-radius - ColorPicker: Mobile-friendly dropdown positioning - README: Complete theme system documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Agent UI
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
Overview
Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces.
Architecture
agent-ui/
├── frontend/ # Vue 3 + Vite + Pinia
│ └── src/
│ ├── components/ # Canvas, Toolbar, StatusBar
│ ├── services/ # Dynamic components system
│ ├── stores/ # Pinia state management
│ └── styles/ # Global CSS
├── server/ # Bun HTTP API + SQLite
└── .mcp.json # MCP server configuration
Quick Start
# Install dependencies
npm install
# Start development (frontend + server)
npm start
- Frontend: http://localhost:4100
- API Server: http://localhost:4101
- WebMCP: ws://localhost:4102
Connecting to Claude Code
- Start the development server:
npm start - In Claude Code, run
/mcpto reconnect - Click the widget (bottom-right) in the browser
- Paste the token from Claude Code
Dynamic Canvas
The dynamic canvas system allows Claude Code to render fully-featured Vue 3 components at runtime.
MCP Tools
render_vue_component
Renders a Vue 3 component directly in the canvas.
{
id: "my-counter", // Unique component ID
name: "MyCounter", // Component name
template: `<div>...</div>`, // Vue template
setup: `...`, // Composition API setup code
style: `.class { ... }`, // Scoped CSS
props: ["title"], // Props list
imports: ["ref", "computed"], // Vue imports needed
componentProps: { title: "Hello" }, // Props values
mode: "replace" | "append" // Render mode
}
save_vue_component
Saves a component to SQLite for later reuse.
{
id: "my-component", // Optional, auto-generated if empty
name: "MyComponent",
template: `...`,
setup: `...`,
style: `...`,
props: [...],
imports: [...]
}
load_vue_component
Loads and renders a previously saved component.
{
id: "my-component",
componentProps: { ... },
mode: "replace" | "append"
}
list_vue_components
Lists all saved components in the database.
delete_vue_component
Deletes a component from the database.
{
id: "my-component"
}
render_html
Renders raw HTML with script/style support (legacy, simpler option).
{
html: "<div>...</div>",
mode: "replace" | "append" | "prepend"
}
Setup Function API
Inside the setup string, components have access to:
Vue Reactivity (via imports)
ref, reactive, computed, watch, watchEffect,
onMounted, onUnmounted, nextTick, provide, inject, h
Props & Context
props // Component props
ctx // { emit, attrs, slots, expose }
Event Bus
$emit(event, ...args) // Emit global event
$on(event, callback) // Listen (returns unsubscribe fn)
$once(event, callback) // Listen once
$off(event, callback) // Stop listening
Utilities
$fetch(url, options) // Native fetch
$nextTick(callback) // Vue nextTick
useCanvasStore() // Pinia store (shared with main app)
Components API
$components.load(id) // Load component from DB
$components.list() // List all components
$components.save(comp) // Save component to DB
Example: Interactive Counter
// MCP call: render_vue_component
{
id: "counter",
name: "Counter",
template: `
<div class="counter">
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="decrement">-</button>
<button @click="increment">+</button>
<p>Double: {{ doubled }}</p>
</div>
`,
setup: `
const count = ref(0);
const doubled = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
onMounted(() => {
console.log('Counter mounted!');
});
return { count, doubled, increment, decrement, title: props.title };
`,
style: `
.counter { padding: 20px; background: #1e1e28; border-radius: 8px; }
.counter button { margin: 0 5px; padding: 8px 16px; }
`,
props: ["title"],
imports: ["ref", "computed", "onMounted"],
componentProps: { title: "My Counter" }
}
Example: Async Data Fetching
{
id: "data-loader",
name: "DataLoader",
template: `
<div class="loader">
<p v-if="loading">Loading...</p>
<pre v-else>{{ data }}</pre>
</div>
`,
setup: `
const loading = ref(true);
const data = ref(null);
// Async setup - uses Suspense internally
const res = await $fetch('http://localhost:4101/api/health');
data.value = await res.json();
loading.value = false;
return { loading, data };
`,
imports: ["ref"]
}
Example: Event Communication
// Component A - Emitter
{
id: "emitter",
name: "Emitter",
template: `<button @click="send">Send Event</button>`,
setup: `
const send = () => {
$emit('my-event', { message: 'Hello!', timestamp: Date.now() });
};
return { send };
`
}
// Component B - Listener
{
id: "listener",
name: "Listener",
template: `<div>Last event: {{ lastEvent }}</div>`,
setup: `
const lastEvent = ref('none');
$on('my-event', (data) => {
lastEvent.value = JSON.stringify(data);
});
return { lastEvent };
`,
imports: ["ref"]
}
Features
CSS Scoping
Styles are automatically scoped to prevent collisions:
/* You write: */
.btn { background: red; }
/* Generated: */
#canvas-content [data-v-abc123] .btn { background: red; }
Async Setup
Components can use await directly in setup - automatically wrapped in Suspense:
setup: `
const data = await $fetch('/api/data');
return { data };
`
Shared State
Components share Pinia stores with the main app:
setup: `
const store = useCanvasStore();
console.log(store.isConnected); // Same state as main app
`
No App Overhead
Components are rendered using render() + createVNode() instead of creating separate Vue apps, sharing the main app's context efficiently.
Technical Notes
Vite Configuration
Runtime template compilation requires this alias in vite.config.ts:
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler.js'
}
}
How It Works
- MCP Tool Call → Claude Code sends component definition via WebSocket
- Build Component →
buildComponent()creates Vue component with dynamicsetupfunction usingnew Function() - CSS Scoping → Styles are transformed:
.btn→#canvas-content [data-v-xxx] .btn - Render →
createVNode()+render()mounts component without creating new Vue app - Context Inheritance → Component inherits
appContextfrom main app (Pinia, directives, etc.)
File Structure
frontend/src/services/dynamicComponents.ts (~300 lines)
├── EventBus # Inter-component communication
├── CSS Scoping # Style isolation
├── Components API # CRUD with server
├── Vue Helpers # What setup() receives
├── buildComponent # Create component from definition
└── renderInlineComponent # Mount to DOM
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/health |
GET | Health check |
/api/history |
GET | Get tool execution history |
/api/history |
POST | Log tool execution |
/api/history |
DELETE | Clear history |
/api/components |
GET | List saved components |
/api/components |
POST | Save component |
/api/components/:id |
GET | Get component by ID |
/api/components/:id |
DELETE | Delete component |
/api/config |
GET/POST | Key-value config storage |
Theme System
Agent UI includes a powerful theming engine with visual editor, presets, and design tokens for consistent styling across dynamic components.
Overview
The theme system provides:
- Visual Editor: Edit CSS variables with live preview
- Presets: System themes (Dark/Light) + custom user themes
- Persistence: Themes saved to SQLite database
- Export/Import: Share themes as JSON files
- Design Tokens: Structured guide for LLM agents to follow
Accessing the Theme Editor
Navigate to /themes in the UI or click the palette icon in the sidebar.
Theme Structure
Themes are organized by categories:
{
"colors": {
"bg-primary": "#0f0f14",
"bg-secondary": "#16161d",
"bg-hover": "#1e1e28",
"bg-tertiary": "#252530",
"border-color": "#2a2a3a"
},
"text": {
"text-primary": "#e4e4e7",
"text-secondary": "#a1a1aa",
"text-muted": "#52525b"
},
"accent": {
"accent": "#6366f1",
"accent-hover": "#818cf8",
"accent-muted": "rgba(99, 102, 241, 0.2)",
"accent-text": "#ffffff"
},
"semantic": {
"success": "#22c55e",
"success-bg": "rgba(34, 197, 94, 0.15)",
"warning": "#eab308",
"warning-bg": "rgba(234, 179, 8, 0.15)",
"error": "#ef4444",
"error-bg": "rgba(239, 68, 68, 0.15)",
"info": "#3b82f6",
"info-bg": "rgba(59, 130, 246, 0.15)"
},
"spacing": {
"radius-sm": "4px",
"radius-md": "8px",
"radius-lg": "12px",
"radius-full": "9999px"
},
"typography": {
"font-sans": "Inter, system-ui, sans-serif",
"font-mono": "JetBrains Mono, monospace"
},
"effects": {
"shadow-sm": "0 1px 2px rgba(0,0,0,0.3)",
"shadow-md": "0 4px 12px rgba(0,0,0,0.4)",
"transition-fast": "0.15s ease"
}
}
MCP Tools
get_design_tokens
Returns design tokens for the active theme. Use this to create components with consistent styling.
{
category: "all" | "colors" | "text" | "accent" | "semantic" | "spacing" | "typography" | "effects"
}
Response example:
Design Tokens del tema "Dark":
[COLORS]
--bg-primary: #0f0f14
--bg-secondary: #16161d
...
GUÍA DE USO:
- Usa var(--nombre-variable) en CSS
- Los componentes dinámicos tienen acceso a $theme.getVariable('nombre')
- Colores semánticos: success, warning, error, info (con -bg para fondos)
- Radius: radius-sm (4px), radius-md (8px), radius-lg (12px), radius-full (9999px)
Theme API in Dynamic Components
Components have access to $theme helper:
// Get CSS variable value
$theme.getVariable('accent') // "#6366f1"
// Set variable temporarily (runtime only)
$theme.setVariable('accent', '#ff0000')
// Get all design tokens
$theme.getTokens()
// Get active theme object
$theme.getActiveTheme()
// Get current variables
$theme.getVariables()
Example: Theme-Aware Component
{
id: "themed-card",
name: "ThemedCard",
template: `
<div class="card">
<h3>{{ title }}</h3>
<p>Current accent: {{ accentColor }}</p>
<button @click="randomizeAccent">Randomize Accent</button>
</div>
`,
setup: `
const accentColor = ref($theme.getVariable('accent'));
const randomizeAccent = () => {
const hue = Math.floor(Math.random() * 360);
const newColor = \`hsl(\${hue}, 70%, 60%)\`;
$theme.setVariable('accent', newColor);
accentColor.value = newColor;
};
return { title: props.title, accentColor, randomizeAccent };
`,
style: `
.card {
padding: var(--radius-lg);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
}
.card h3 { color: var(--text-primary); }
.card p { color: var(--text-secondary); }
.card button {
background: var(--accent);
color: var(--accent-text);
border: none;
padding: 0.5rem 1rem;
border-radius: var(--radius-sm);
cursor: pointer;
}
.card button:hover { background: var(--accent-hover); }
`,
props: ["title"],
imports: ["ref"],
componentProps: { title: "Themed Card" }
}
API Endpoints (Themes)
| Endpoint | Method | Description |
|---|---|---|
/api/themes |
GET | List all themes |
/api/themes |
POST | Create/update theme |
/api/themes/active |
GET | Get active (default) theme |
/api/themes/:id |
GET | Get theme by ID |
/api/themes/:id |
DELETE | Delete theme |
/api/themes/:id/default |
POST | Set as default theme |
/api/themes/export/:id |
GET | Export theme as JSON |
/api/design-tokens |
GET | Get design tokens guide |
Theme Editor Features
Desktop View
- Sidebar: List of system and custom themes
- Editor: Category tabs (Colors, Text, Accent, etc.)
- Variables Grid: Color pickers and input fields
- Live Preview: Buttons, cards, badges, inputs
Mobile View
- Compact Header: Theme dropdown + action buttons
- Collapsible Variables: Toggle to show/hide editors
- Responsive Preview: Adapts to screen size
Actions
- Save: Saves current changes (creates copy if editing system theme)
- Reset: Reverts unsaved changes
- Export: Downloads theme as JSON file
- Clone: Creates a copy of any theme
- Set Default: Makes theme load on startup
File Structure
frontend/src/
├── stores/theme.ts # Pinia store
├── services/themeService.ts # API client + utilities
├── pages/ThemesPage.vue # Main editor page
└── components/themes/
├── ColorPicker.vue # HSL color picker
├── VariableEditor.vue # Variable input (color/size/text)
├── ThemePreview.vue # Live preview component
└── ThemeListItem.vue # Theme list item with actions
Tech Stack
- Frontend: Vue 3, Vite, Pinia, TypeScript
- Server: Bun, SQLite
- MCP: @nucleoriofrio/webmcp
- PWA: vite-plugin-pwa
License
MIT