josedario87 075e167389 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>
2026-02-13 04:15:53 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00

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

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.

{
  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

  1. MCP Tool Call → Claude Code sends component definition via WebSocket
  2. Build ComponentbuildComponent() creates Vue component with dynamic setup function using new Function()
  3. CSS Scoping → Styles are transformed: .btn#canvas-content [data-v-xxx] .btn
  4. RendercreateVNode() + 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

Description
Agent UI - WebMCP Canvas
Readme 3.3 MiB
Languages
Vue 58.4%
TypeScript 36.9%
Kotlin 3.1%
Python 0.6%
Rust 0.3%
Other 0.6%