- Add Nucleo atom logo with animated orbiting electrons - Redesign FAB with glassmorphism effect and purple gradient - Add connection indicator (green dot) when terminal is open - Update FloatingTerminal header with Nucleo branding - Add PermissionRequest hook support with red alert animation - Document Nucleo in README with visual states table - Create official Nucleo logo SVG in docs/
650 lines
15 KiB
Markdown
650 lines
15 KiB
Markdown
# Agent UI
|
|
|
|
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
|
|
|
|
---
|
|
|
|
## Nucleo
|
|
|
|
<p align="center">
|
|
<img src="docs/nucleo-logo.svg" alt="Nucleo" width="120"/>
|
|
</p>
|
|
|
|
**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: `<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.
|
|
|
|
```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: "<div>...</div>",
|
|
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: `
|
|
<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
|
|
|
|
```javascript
|
|
{
|
|
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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```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: `
|
|
<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
|