# Agent UI Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol). --- ## Nucleo

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: ```json { "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 ```bash # 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 1. Start the development server: `npm start` 2. In Claude Code, run `/mcp` to reconnect 3. Click the widget (bottom-right) in the browser 4. 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. ```javascript { id: "my-counter", // Unique component ID name: "MyCounter", // Component name template: `
...
`, // 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. ```javascript { 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. ```javascript { 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. ```javascript { id: "my-component" } ``` ### `render_html` Renders raw HTML with script/style support (legacy, simpler option). ```javascript { html: "
...
", mode: "replace" | "append" | "prepend" } ``` --- ## Setup Function API Inside the `setup` string, components have access to: ### Vue Reactivity (via `imports`) ```javascript ref, reactive, computed, watch, watchEffect, onMounted, onUnmounted, nextTick, provide, inject, h ``` ### Props & Context ```javascript props // Component props ctx // { emit, attrs, slots, expose } ``` ### Event Bus ```javascript $emit(event, ...args) // Emit global event $on(event, callback) // Listen (returns unsubscribe fn) $once(event, callback) // Listen once $off(event, callback) // Stop listening ``` ### Utilities ```javascript $fetch(url, options) // Native fetch $nextTick(callback) // Vue nextTick useCanvasStore() // Pinia store (shared with main app) ``` ### Components API ```javascript $components.load(id) // Load component from DB $components.list() // List all components $components.save(comp) // Save component to DB ``` --- ## Example: Interactive Counter ```javascript // MCP call: render_vue_component { id: "counter", name: "Counter", template: `

{{ title }}

Count: {{ count }}

Double: {{ doubled }}

`, 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 ```javascript { id: "data-loader", name: "DataLoader", template: `

Loading...

{{ data }}
`, 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 ```javascript // Component A - Emitter { id: "emitter", name: "Emitter", template: ``, setup: ` const send = () => { $emit('my-event', { message: 'Hello!', timestamp: Date.now() }); }; return { send }; ` } // Component B - Listener { id: "listener", name: "Listener", template: `
Last event: {{ lastEvent }}
`, 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: ```css /* 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: ```javascript setup: ` const data = await $fetch('/api/data'); return { data }; ` ``` ### Shared State Components share Pinia stores with the main app: ```javascript 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`: ```typescript resolve: { alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } } ``` ### How It Works 1. **MCP Tool Call** → Claude Code sends component definition via WebSocket 2. **Build Component** → `buildComponent()` creates Vue component with dynamic `setup` function using `new Function()` 3. **CSS Scoping** → Styles are transformed: `.btn` → `#canvas-content [data-v-xxx] .btn` 4. **Render** → `createVNode()` + `render()` mounts component without creating new Vue app 5. **Context Inheritance** → Component inherits `appContext` from 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: ```javascript { "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. ```javascript { 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: ```javascript // 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 ```javascript { id: "themed-card", name: "ThemedCard", template: `

{{ title }}

Current accent: {{ accentColor }}

`, 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