- Tools only register when connected to MCP (has torch) - Store current page for tool activation when torch is obtained - Add onTorchConnected to activate page tools after MCP connection - Add onTorchDisconnected to clear tools when losing torch - Page changes only update tools if connected, otherwise store for later
Agent UI
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
Nucleo
Nucleo is the main AI agent powering Agent UI. It serves as the bridge between Claude Code and your visual interface, providing real-time status feedback through an animated FAB (Floating Action Button).
Visual States
Nucleo communicates its current state through distinct animations:
| State | Color | Animation | Trigger |
|---|---|---|---|
| Idle | Purple | Rotating atom | Default state |
| Processing | Orange | Pulsing dots | User prompt submitted |
| Reading | Cyan | Eye icon + scan | Reading files (Read/Glob/Grep) |
| Writing | Green | Pencil icon + pulse | Writing files (Edit/Write) |
| Subagent | Purple | Orbital ring | Task tool spawned |
| Permission | Red | Alert + shake | Awaiting user permission |
| Session Start | Green | Ripple waves | Session initialized |
| Notification | Yellow | Bounce | System notification |
Integration
Nucleo's status is synchronized via Claude Code hooks:
{
"hooks": {
"UserPromptSubmit": [{ "hooks": [{ "command": "... status: processing ..." }] }],
"PreToolUse": [{ "hooks": [{ "command": "... status: toolUse ..." }] }],
"PostToolUse": [{ "hooks": [{ "command": "... status: toolDone ..." }] }],
"Stop": [{ "hooks": [{ "command": "... status: idle ..." }] }]
}
}
The FAB receives these status updates via WebSocket and displays the corresponding animation, giving you real-time visibility into what Claude is doing.
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