Tony openui-dashboard

Generate dashboard UIs using the OpenUI spec (openui.com) with a React + DuckDB backend. Use this skill when building dashboards, data visualization UIs, chat interfaces, or any LLM-driven UI generation. Includes pre-built chat session support for TheGardener.

install
source · Clone the upstream repo
git clone https://github.com/jaydeland/Tony
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jaydeland/Tony "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/openui-dashboard" ~/.claude/skills/jaydeland-tony-openui-dashboard && rm -rf "$T"
manifest: .claude/skills/openui-dashboard/skill.md
source content

OpenUI Dashboard Generation Skill

This skill generates dashboard UIs by combining the OpenUI spec (openui.com / github.com/thesysdev/openui) with a React frontend and DuckDB backend. The workflow is:

DuckDB (local) → Orchestrator Agent → OpenUI Lang output → React Renderer → Dashboard

The orchestrator agent (or any LLM) is given a component library and outputs OpenUI Lang — a code-like streaming syntax — instead of raw JSX or HTML. The React runtime parses and renders it progressively.


Step 1 — Understand OpenUI Core Concepts

What OpenUI Is

OpenUI is a rendering specification for LLM-generated interfaces. It has three layers:

LayerWhat it does
OpenUI LangA compact code-like syntax LLMs output (not JSON — ~67% fewer tokens)
@openuidev/react-lang
Parser + renderer — converts streaming OpenUI Lang into React components
@openuidev/react-ui
Prebuilt chat UI + component library (charts, tables, forms)

OpenUI Lang Syntax (Basics)

root = Stack([title, StatCard(label="Agents", value="3")])
title = TextContent("Dashboard")
  • No JSON — syntax resembles Python/TypeScript function calls
  • Positional args — key order from Zod schema determines arg order
  • Streaming-first — root renders immediately; children stream in
  • Root constraint — output always starts with the library's declared root component

Defining Components

import { defineComponent, createLibrary } from "@openuidev/react-lang";
import { z } from "zod";

// 1. Define each component with Zod props schema
const StatCard = defineComponent({
  name: "StatCard",                          // used in OpenUI Lang output
  description: "Displays a metric label and value.",  // seen by the LLM
  props: z.object({
    label: z.string(),
    value: z.string(),
  }),
  component: ({ props }) => (
    <div className="stat-card">
      <strong>{props.label}</strong>
      <div className="value">{props.value}</div>
    </div>
  ),
});

// 2. For nested children, use .ref
const Item = defineComponent({
  name: "Item",
  props: z.object({ label: z.string() }),
  component: ({ props }) => <div>{props.label}</div>,
});

const List = defineComponent({
  name: "List",
  props: z.object({ items: z.array(Item.ref) }),  // .ref enables nesting
  component: ({ props, renderNode }) => (
    <div>{renderNode(props.items)}</div>          // renderNode handles children
  ),
});

// 3. Compose into a library
export const dashboardLibrary = createLibrary({
  root: "Stack",           // entry point — output always wrapped in Stack
  components: [StatCard, Item, List],
  componentGroups: [
    { name: "Stats", components: ["StatCard"] },
    { name: "Layout", components: ["Stack", "List"] },
  ],
});

Generating the System Prompt

import { dashboardLibrary, openuiPromptOptions } from "./dashboard-library";

// Generate the instruction text the LLM needs
const systemPrompt = dashboardLibrary.prompt({
  preamble: "You are a dashboard generator. Output only OpenUI Lang.",
  additionalRules: ["Always wrap the root in Stack.", "Use StatCard for metrics."],
  examples: [
    `root = Stack([StatCard(label="Active Agents", value="4")])`,
  ],
});

Streaming Renderer (React)

import { OpenUIStreamRenderer } from "@openuidev/react-lang";

function DashboardPage() {
  return (
    <OpenUIStreamRenderer
      library={dashboardLibrary}
      onError={(err) => console.error("Parse error:", err)}
    >
      {/* children receive the streamed output as it's parsed */}
    </OpenUIStreamRenderer>
  );
}

Step 2 — DuckDB Integration Pattern

DuckDB runs locally. The orchestrator agent queries it, then passes results to the LLM for visualization.

Python: Query DuckDB + Format for OpenUI

import duckdb

def query_dashboard_data(sql: str) -> dict:
    """Execute DuckDB query, return results as dict for OpenUI generation."""
    conn = duckdb.connect("openseed.db")
    result = conn.execute(sql).fetchdf()
    conn.close()
    return {
        "columns": result.columns.tolist(),
        "rows": result.to_dict(orient="records"),
        "row_count": len(result),
    }

# Example: get agent run stats
stats = query_dashboard_data("""
    SELECT agent_name, COUNT(*) as runs,
           SUM(cost_usd) as total_cost,
           MAX(started_at) as last_run
    FROM runs GROUP BY agent_name
""")
# stats → {"columns": ["agent_name", "runs", ...], "rows": [...], "row_count": N}

What to Pass to the LLM

After querying DuckDB, give the LLM:

  1. Component library definition (the
    defineComponent
    +
    createLibrary
    code above)
  2. DuckDB results (as JSON — columns, rows)
  3. Task prompt e.g., "Generate a dashboard showing agent activity from this data"

Step 3 — Dashboard Component Library Template

Here is a ready-to-use OpenUI component library for OpenSeed-style dashboards. It covers the 7 DB tables from the OpenSeed schema.

// dashboard-library.ts
import { defineComponent, createLibrary } from "@openuidev/react-lang";
import { z } from "zod";

// ── Layout Components ────────────────────────────────────────────────────────

const Stack = defineComponent({
  name: "Stack",
  description: "Vertical stack layout — root container for dashboard sections.",
  props: z.object({
    children: z.array(z.any()).optional(),
  }),
  component: ({ props, renderNode }) => (
    <div className="flex flex-col gap-4 p-4">
      {props.children ? renderNode(props.children) : null}
    </div>
  ),
});

// ── Stat / Metric Components ────────────────────────────────────────────────

const StatCard = defineComponent({
  name: "StatCard",
  description: "Single metric tile — label + large value + optional delta.",
  props: z.object({
    label: z.string(),
    value: z.string(),
    delta: z.string().optional(),
    delta_positive: z.boolean().optional(),
  }),
  component: ({ props }) => (
    <div className="bg-white rounded shadow p-4 border">
      <div className="text-gray-500 text-sm">{props.label}</div>
      <div className="text-2xl font-bold mt-1">{props.value}</div>
      {props.delta && (
        <div className={`text-xs mt-1 ${props.delta_positive ? "text-green-600" : "text-red-600"}`}>
          {props.delta}
        </div>
      )}
    </div>
  ),
});

const StatGrid = defineComponent({
  name: "StatGrid",
  description: "Responsive grid of StatCard components.",
  props: z.object({
    items: z.array(StatCard.ref),
  }),
  component: ({ props, renderNode }) => (
    <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
      {renderNode(props.items)}
    </div>
  ),
});

// ── Table Component ──────────────────────────────────────────────────────────

const DataTable = defineComponent({
  name: "DataTable",
  description: "Sortable table with column headers and row data.",
  props: z.object({
    columns: z.array(z.string()),
    rows: z.array(z.record(z.string(), z.any())),
  }),
  component: ({ props }) => (
    <div className="overflow-x-auto bg-white rounded shadow">
      <table className="min-w-full text-sm">
        <thead className="bg-gray-50 border-b">
          <tr>
            {props.columns.map((col) => (
              <th key={col} className="px-4 py-2 text-left font-medium text-gray-600">
                {col}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {props.rows.map((row, i) => (
            <tr key={i} className="border-b hover:bg-gray-50">
              {props.columns.map((col) => (
                <td key={col} className="px-4 py-2">{String(row[col] ?? "—")}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  ),
});

// ── Chart Component (Bar) ───────────────────────────────────────────────────

const BarChart = defineComponent({
  name: "BarChart",
  description: "Horizontal bar chart for categorical data.",
  props: z.object({
    title: z.string(),
    data: z.array(z.object({ label: z.string(), value: z.number() })),
  }),
  component: ({ props }) => {
    const max = Math.max(...props.data.map((d) => d.value));
    return (
      <div className="bg-white rounded shadow p-4">
        <div className="font-medium mb-3">{props.title}</div>
        <div className="flex flex-col gap-2">
          {props.data.map((item) => (
            <div key={item.label} className="flex items-center gap-2">
              <div className="w-24 text-sm truncate">{item.label}</div>
              <div className="flex-1 bg-gray-100 rounded h-5 overflow-hidden">
                <div
                  className="bg-blue-500 h-full rounded"
                  style={{ width: `${(item.value / max) * 100}%` }}
                />
              </div>
              <div className="text-sm w-12 text-right">{item.value}</div>
            </div>
          ))}
        </div>
      </div>
    );
  },
});

// ── Status Badge ─────────────────────────────────────────────────────────────

const StatusBadge = defineComponent({
  name: "StatusBadge",
  description: "Colored badge for status labels (active, pending, error, done).",
  props: z.object({
    label: z.string(),
    status: z.enum(["active", "pending", "error", "done", "running"]),
  }),
  component: ({ props }) => {
    const colors = {
      active: "bg-green-100 text-green-800",
      pending: "bg-yellow-100 text-yellow-800",
      error: "bg-red-100 text-red-800",
      done: "bg-gray-100 text-gray-800",
      running: "bg-blue-100 text-blue-800",
    };
    return (
      <span className={`px-2 py-1 rounded text-xs font-medium ${colors[props.status]}`}>
        {props.label}
      </span>
    );
  },
});

// ── Timeline / Activity Feed ────────────────────────────────────────────────

const ActivityFeed = defineComponent({
  name: "ActivityFeed",
  description: "Vertical timeline of events with timestamps.",
  props: z.object({
    items: z.array(z.object({
      time: z.string(),
      agent: z.string(),
      action: z.string(),
      status: z.enum(["active", "pending", "error", "done", "running"]).optional(),
    })),
  }),
  component: ({ props }) => (
    <div className="bg-white rounded shadow divide-y">
      {props.items.map((item, i) => (
        <div key={i} className="p-3 flex items-start gap-3">
          <div className="mt-1">{item.status && <StatusBadge label={item.status} status={item.status} />}</div>
          <div className="flex-1 min-w-0">
            <div className="text-sm font-medium">{item.agent}</div>
            <div className="text-sm text-gray-600">{item.action}</div>
          </div>
          <div className="text-xs text-gray-400 whitespace-nowrap">{item.time}</div>
        </div>
      ))}
    </div>
  ),
});

// ── Gate Card ────────────────────────────────────────────────────────────────

const GateCard = defineComponent({
  name: "GateCard",
  description: "Gate approval request with action description and context.",
  props: z.object({
    id: z.string(),
    action: z.string(),
    description: z.string(),
    created_at: z.string(),
    context: z.record(z.string(), z.any()).optional(),
  }),
  component: ({ props }) => (
    <div className="bg-yellow-50 border border-yellow-200 rounded p-4">
      <div className="flex items-center justify-between mb-2">
        <span className="font-bold text-yellow-800">{props.action}</span>
        <StatusBadge label="PENDING" status="pending" />
      </div>
      <p className="text-sm text-gray-700 mb-2">{props.description}</p>
      <div className="text-xs text-gray-400">Created: {props.created_at}</div>
    </div>
  ),
});

// ── Library Export ───────────────────────────────────────────────────────────

export const dashboardLibrary = createLibrary({
  root: "Stack",
  components: [
    Stack, StatCard, StatGrid, DataTable, BarChart,
    StatusBadge, ActivityFeed, GateCard,
  ],
  componentGroups: [
    {
      name: "Stats",
      components: ["StatCard", "StatGrid"],
      notes: ["Use StatCard for single metrics.", "Use StatGrid for multiple related metrics."],
    },
    {
      name: "Data Display",
      components: ["DataTable", "BarChart", "ActivityFeed"],
      notes: ["DataTable for tabular data.", "BarChart for categorical comparisons."],
    },
    {
      name: "Status",
      components: ["StatusBadge"],
      notes: ["Use StatusBadge inside other components, not as root."],
    },
    {
      name: "Gates",
      components: ["GateCard"],
      notes: ["GateCard for pending approval items."],
    },
  ],
});

Step 4 — Orchestrator Agent Integration

The orchestrator agent (the Gardener in OpenSeed) generates the OpenUI Lang output. Here is how to wire it up:


Step 3.5 — Chat Session Implementation (NEW)

The dashboard skill now includes full chat session support for TheGardener. This implementation provides:

Chat Server (
web/server.py
)

# FastAPI chat server with WebSocket streaming
from fastapi import FastAPI, WebSocket
from web.server import ChatServer, ChatDatabase

# Routes:
# POST /api/chat/session     - Create new chat session
# GET  /api/chat/sessions    - List all sessions
# GET  /api/chat/{id}/history - Get conversation history
# POST /api/chat/{id}/message - Send message, stream SSE response
# WS   /api/chat/{id}/ws      - WebSocket real-time streaming

Chat UI (
web/ChatApp.tsx
)

React chat interface with:

  • Session sidebar (create/select sessions)
  • Message feed with user/assistant styling
  • SSE streaming for real-time responses
  • WebSocket support for bidirectional communication

Component Library (
web/dashboard-library.ts
)

Pre-built chat components:

  • ChatMessage
    - Role-based message styling
  • ChatFeed
    - Scrollable message container
  • ChatInput
    - Text input with send button
  • SessionCard
    - Session list item
  • StatusBadge
    - Status indicator

Running the Chat Server

# Terminal 1 - Start chat server
cd web
pip install -r requirements.txt
python server.py

# Terminal 2 - Start React dev server
cd web
npm install
npm run dev

# Open http://localhost:5173

TheGardener Integration

Chat messages flow through TheGardener's classification system:

  1. User sends message via chat UI
  2. Server stores message in
    chat_message
    table
  3. TheGardener classifies content keywords
  4. Delegates to appropriate skill agent (gsd-2, obsidian, dashboard, etc.)
  5. Response streams back via SSE/WebSocket
  6. Stored in conversation history

Step 4 — Orchestrator Agent Integration (Original)

The orchestrator agent (the Gardener in OpenSeed) generates the OpenUI Lang output. Here is how to wire it up:

System Prompt Fragment

Give the orchestrator this system prompt addition:

You are an OpenUI dashboard generator. When asked to generate a dashboard, you MUST output **only** valid OpenUI Lang syntax.

**Rules:**
1. Output starts with `root = ` followed by the root component name
2. Use positional arguments for props (order matches Zod schema key order)
3. String values use double quotes
4. Arrays use square brackets `[]`
5. Children use square bracket notation: `Stack([StatCard(...), DataTable(...)])`

**Available components** (from the dashboard library):
- Stack — vertical layout container
- StatCard — single metric (label, value, optional delta)
- StatGrid — grid of StatCard
- DataTable — sortable table (columns array, rows array)
- BarChart — horizontal bar chart (title, data array)
- ActivityFeed — timeline of events
- StatusBadge — colored status badge
- GateCard — gate approval request

**Example output:**

root = Stack([ StatGrid([ StatCard(label="Active Agents", value="3"), StatCard(label="Runs Today", value="12"), StatCard(label="Daily Cost", value="$2.47"), StatCard(label="Pending Gates", value="1"), ]), ActivityFeed([ { time="10:32", agent="coder-1", action="Committed hello.py", status="done" }, { time="10:28", agent="monitor-1", action="Generated dashboard", status="done" }, ]), ])

Python: Orchestrator → OpenUI Lang → React

# openseed/outputs/dashboard.py (sketch)
import httpx

DASHBOARD_API = "http://localhost:8420"

def generate_dashboard_openui(data: dict, model: str = "claude-sonnet-4") -> str:
    """
    Ask the LLM (via OpenAI-compatible API) to generate OpenUI Lang
    from DuckDB query results + component library.
    Returns the OpenUI Lang string.
    """
    system_prompt = _load_dashboard_system_prompt()
    user_prompt = f"Generate a dashboard for this data:\n{_pformat(data)}\n\nOutput only OpenUI Lang."
    response = httpx.post("https://api.anthropic.com/v1/messages", json={
        "model": model,
        "max_tokens": 4096,
        "system": system_prompt,
        "messages": [{"role": "user", "content": user_prompt}],
    })
    return response.json()["content"][0]["text"]


def serve_dashboard(openui_lang: str, library_id: str = "dashboard"):
    """POST OpenUI Lang to the dashboard render endpoint."""
    httpx.post(f"{DASHBOARD_API}/api/render", json={
        "lang": openui_lang,
        "library": library_id,
    })


def dashboard_cycle(db_path: str = "openseed.db"):
    """Full cycle: query DuckDB → generate OpenUI Lang → push to dashboard."""
    from openseed.db import Database
    db = Database(db_path)
    # Query key stats
    stats = db.query("""
        SELECT agent_name, COUNT(*) as runs, SUM(cost_usd) as cost
        FROM runs GROUP BY agent_name
    """)
    signals = db.query("SELECT * FROM signals ORDER BY created_at DESC LIMIT 20")
    gates = db.query("SELECT * FROM gates WHERE status = 'pending'")
    db.close()

    # Format data for LLM
    data = {"stats": stats, "signals": signals, "gates": gates}
    openui_lang = generate_dashboard_openui(data)

    serve_dashboard(openui_lang)
    return openui_lang

React Dashboard Renderer (Frontend)

// DashboardApp.tsx
import { useState } from "react";
import { dashboardLibrary } from "./dashboard-library";

async function streamDashboard(openuiLang: string) {
  const res = await fetch("/api/render", {
    method: "POST",
    body: JSON.stringify({ lang: openuiLang }),
  });
  // The response is a streaming text/event-stream of OpenUI Lang tokens
  // Use @openuidev/react-lang's streaming parser to render progressively
  return res.body;
}

export default function DashboardApp() {
  const [rendered, setRendered] = useState<React.ReactNode>(null);

  function handleOpenUIStream(stream: ReadableStream) {
    const reader = stream.getReader();
    const decoder = new TextDecoder();
    const parser = new OpenUIParser(dashboardLibrary);

    function pump() {
      reader.read().then(({ done, value }) => {
        if (done) return;
        const text = decoder.decode(value);
        parser.feed(text);           // parse incremental chunk
        setRendered(parser.render()); // re-render on each parsed statement
        pump();
      });
    }
    pump();
  }

  return (
    <div className="min-h-screen bg-gray-50">
      {rendered || <div className="p-4 text-gray-500">Loading dashboard...</div>}
    </div>
  );
}

Step 5 — Prompt Patterns for Dashboard Generation

Pattern 1: Stats Overview

Generate a stats overview dashboard showing:
- Total runs today
- Active agents count
- Total cost today
- Pending signals count
Use StatGrid with StatCard for each metric.

Pattern 2: Agent Activity Feed

Show a timeline of recent agent activity:
- Filter to last 24 hours
- Include agent name, action description, timestamp, status
Use ActivityFeed with StatusBadge for each entry.

Pattern 3: Cost Breakdown

Visualize daily cost by agent as a bar chart.
Show top 10 agents by cumulative cost.
Use BarChart with agent names as labels.

Pattern 4: Gate Queue

Display all pending gates in a stacked layout.
Each gate should show: action type, description, created time.
Use GateCard for each pending gate.

Pattern 5: Signals Table

Show recent signals in a sortable table with columns:
source, type, status, created_at.
Use DataTable with the signals data.

Step 6 — DuckDB → Dashboard End-to-End Flow

┌─────────────┐     ┌──────────────┐     ┌──────────────────┐
│  DuckDB     │────▶│  Orchestrator │────▶│  OpenUI Lang     │
│  openseed.db│     │  (Gardener)   │     │  (streaming text)│
└─────────────┘     └──────────────┘     └────────┬─────────┘
                                                  │ stream
                                                  ▼
┌─────────────┐     ┌──────────────┐     ┌──────────────────┐
│  Dashboard   │◀────│  React       │◀────│  Parser + Render  │
│  (browser)   │     │  Renderer    │     │  @openuidev/lang │
└─────────────┘     └──────────────┘     └──────────────────┘
  1. DuckDB holds all OpenSeed state (signals, runs, costs, gates)
  2. Orchestrator queries DuckDB → builds prompt with data + library → calls LLM
  3. LLM outputs OpenUI Lang — streaming, incremental text
  4. React renderer parses and renders progressively — no full round-trip needed

Quick Reference

WhatHow
Define component
defineComponent({ name, description, props: z.object({...}), component })
Enable children
z.array(ChildComponent.ref)
+
renderNode(props.children)
Nest in promptUse
.ref
— the schema includes the child component name for LLM discovery
Root componentSet
root: "Stack"
in
createLibrary
— LLM always wraps output
Generate prompt
library.prompt({ preamble, additionalRules, examples })
Group components
componentGroups
array — helps LLM find related components
Style componentsUse Tailwind classes in the
component
render function
Key orderingProps key order in Zod schema = positional arg order in OpenUI Lang
Union children
z.union([CompA.ref, CompB.ref])
— for polymorphic children
StreamingOpenUI Lang streams token-by-token; root renders immediately

See Also