josedario87 ba4a1a0059 fix: validate transcript sessions before resume and fix FAB race condition
Server now checks that transcript .jsonl files exist before creating
terminals, preventing dead sessions from --resume errors. Frontend
shows error banner in modal when resume fails. Fixed race condition
where init() would overwrite FAB terminal selection after page refresh
by guarding with pendingSwitchTarget flag.
2026-02-21 12:51:15 -06:00
2026-02-13 21:28:56 -06:00
2026-02-13 03:10:06 -06:00
2026-02-18 12:13:22 -06:00

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:

{
  "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

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


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

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%