feat: Add dynamic Vue 3 components system
- Add dynamicComponents.ts service (~300 lines) - CSS scoping with high specificity - Async setup support with Suspense - Event bus for inter-component communication - Shared Pinia store with main app - No app overhead (uses render + createVNode) - Add MCP tools for Vue components - render_vue_component - save_vue_component - load_vue_component - list_vue_components - delete_vue_component - Add SQLite table for component persistence - Add TypeScript declarations for webmcp - Configure Vite for runtime template compilation - Add comprehensive README with documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
381
README.md
Normal file
381
README.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# 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
|
||||
|
||||
```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 |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3, Vite, Pinia, TypeScript
|
||||
- **Server**: Bun, SQLite
|
||||
- **MCP**: @nucleoriofrio/webmcp
|
||||
- **PWA**: vite-plugin-pwa
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user