# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- app/docs/overview/content.mdx ---
import { DocsHeader } from "../_components/docs-header";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { FlaskConical } from "lucide-react";

<DocsHeader
  title="Overview"
  description="A React component framework for conversation-native UIs."
  mdxPath="app/docs/overview/content.mdx"
/>

<Alert className="squircle my-6 flex max-w-2xl flex-col gap-3 rounded-4xl border-emerald-300 bg-emerald-50/50 p-5 dark:border-emerald-950 dark:bg-emerald-950/20">
  <AlertTitle className="mb-0! flex items-center gap-2 pb-0! font-mono text-emerald-700 dark:text-emerald-300">
    <FlaskConical className="size-4 text-emerald-600! dark:text-emerald-300!" />
    {"Research Preview"}
  </AlertTitle>
  <AlertDescription className="my-0! py-0! text-pretty text-emerald-800 dark:text-emerald-100">
    {
      "Tool UI is in active development, and APIs are still coalescing as we refine discover better patterns for conversation-native UIs."
    }
  </AlertDescription>
</Alert>

Tool UI is a React component framework for **conversation‑native** UIs.  
Tools return **JSON**; Tool UI renders it as **inline, narrated, referenceable** surfaces.

## At a glance

- **Conversation‑native components** that live _inside messages_, optimized for chat width and scroll.
- **Schema‑first rendering**: every surface is driven by a serializable schema with stable IDs.
- **Assistant‑anchored**: the assistant introduces, interprets, and closes each surface.
- **Stack‑agnostic**: works with any LLM/provider and orchestration layer.

## Where it fits

[Radix](https://www.radix-ui.com/)/[shadcn](https://ui.shadcn.com/) (primitives) → **Tool UI** (conversation‑native components & schema) → [AI SDK](https://ai-sdk.dev/) / [LangGraph](https://langchain-ai.github.io/langgraphjs/) / etc. (LLM orchestration)

## Who it’s for

- **React devs & design engineers** building LLM chat apps.
- Teams who want **predictable, accessible** UI that the assistant can **reference** (“the second row”).
- Anyone who wants **typed, schema‑validated** tool outputs instead of brittle HTML strings.

## Why Tool UI

- **Predictable rendering**: Tools emit schema‑validated JSON; components render consistently across themes/devices.
- **Built for chat**: Mobile‑first, glanceable, minimal chrome; no in‑message navigation flows.
- **Developer control**: Components render; **your app** owns side effects via callbacks/server actions.
- **Type safety**: Serializable schemas on the server, parsers on the client; works great with AI SDK [`InferUITools`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/infer-ui-tools).
- **Lifecycle aware**: Clear phases from invocation through receipt; supersession behavior is explicit.

## Mental model

See **[UI Guidelines](/docs/design-guidelines)** for the full philosophy. Quick summary:

- **5 Fundamentals:** One intent · Inline · Schema‑first · Assistant‑anchored · Lifecycle aware
- **Roles:** `information`, `decision`, `control`, `state`, `composite`
- **Lifecycle:** `invocation → output‑pending → interactive → committing → receipt → errored`
- **Conversation coherence:** Every important action ↔ one **canonical sentence**; side effects yield a **durable receipt** (status, summary, identifiers, timestamp)

## How it works

The assistant calls a tool, the tool returns JSON matching a schema, and the UI renders inline.

<Mermaid chart={`
flowchart LR
    A["Assistant calls tool"] --> B["JSON output"]
    B --> C["‹Component /›"]
    C --> D["User interacts"]
    D --> E["App handles effects"]

    style A fill:#8b5cf6,stroke:#7c3aed,stroke-width:2px,color:#fff
    style B fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
    style C fill:#3b82f6,stroke:#2563eb,stroke-width:2px,color:#fff
    style D fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
    style E fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff

`} />

### Minimal example

**Server side:** Define a tool that returns schema-validated JSON.

**What this demonstrates:**

- `SerializableLinkPreviewSchema` ensures type-safe output
- Actions are defined with both UI labels and canonical sentences for conversation coherence
- The schema guarantees the frontend receives reconstructable, addressable data

```ts title="Server: define a tool with an output schema"
import { streamText, tool, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { SerializableLinkPreviewSchema } from "@/components/tool-ui/link-preview/schema";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages: convertToModelMessages(messages),
    tools: {
      previewLink: tool({
        description: "Show a preview card for a URL",
        inputSchema: z.object({ url: z.url() }),
        outputSchema: SerializableLinkPreviewSchema,
        async execute({ url }) {
          return {
            id: "link-preview-1",
            href: url,
            title: "Example Site",
            description: "A description of the linked content",
            image: "https://example.com/image.jpg",
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}
```

**Client side:** Register the component and let assistant-ui handle rendering.

**What this demonstrates:**

- `makeAssistantToolUI` connects the tool name to a React component
- `parseSerializableLinkPreview` validates the tool result at runtime and returns typed props
- Simply mounting `<PreviewLinkUI />` registers it (no manual plumbing required)
- assistant-ui manages the runtime, streaming, and tool call lifecycle

```tsx title="Client: render with assistant-ui"
"use client";
import {
  AssistantRuntimeProvider,
  makeAssistantToolUI,
} from "@assistant-ui/react";
import {
  useChatRuntime,
  AssistantChatTransport,
} from "@assistant-ui/react-ai-sdk";
import {
  LinkPreview,
  parseSerializableLinkPreview,
} from "@/components/tool-ui/link-preview";

const PreviewLinkUI = makeAssistantToolUI({
  toolName: "previewLink",
  render: ({ result }) => {
    const preview = parseSerializableLinkPreview(result);
    return <LinkPreview {...preview} maxWidth="420px" />;
  },
});

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <PreviewLinkUI />
      {/* your <Thread /> component here */}
    </AssistantRuntimeProvider>
  );
}
```

## Anatomy of a surface

Every Tool UI surface is addressable and reconstructable. Here's the common schema structure:

**What this shows:**

- `id` makes the tool UI referenceable by the assistant ("the link preview above")
- `role` declares the primary purpose (information, decision, control, state, or composite)
- `actions` with `sentence` fields enable natural language interaction
- `receipt` provides durable proof of side effects with timestamps and identifiers

```ts
{
  id: string;                     // stable identifier for this rendering
  role: "information"|"decision"|"control"|"state"|"composite";
  actions?: Array<{ id: string; label: string; sentence: string }>;
  // optional receipt after side effects:
  receipt?: {
    outcome: "success"|"partial"|"failed"|"cancelled";
    summary: string;
    identifiers?: Record<string, string>;
    at: string;                   // ISO timestamp
  };
}
```

## Next steps

- **[Quick Start](/docs/quick-start)**: Wire your first Tool UI
- **[UI Guidelines](/docs/design-guidelines)**: Patterns, roles, lifecycle, receipts
- **[Components](/docs/data-table)**: Explore Data Table, Image, Video, Link Preview, Option List, Social Post


## Links discovered
- [Radix](https://www.radix-ui.com/)
- [shadcn](https://ui.shadcn.com/)
- [AI SDK](https://ai-sdk.dev/)
- [LangGraph](https://langchain-ai.github.io/langgraphjs/)
- [`InferUITools`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/infer-ui-tools)
- [UI Guidelines](https://github.com/assistant-ui/tool-ui/blob/main/docs/design-guidelines.md)
- [Quick Start](https://github.com/assistant-ui/tool-ui/blob/main/docs/quick-start.md)
- [Components](https://github.com/assistant-ui/tool-ui/blob/main/docs/data-table.md)

--- lib/docs/component-registry.ts ---
export interface ComponentMeta {
  id: string;
  label: string;
  description: string;
  path: string;
}

export const componentsRegistry: ComponentMeta[] = [
  {
    id: "chart",
    label: "Chart",
    description: "Visualize data with interactive charts",
    path: "/docs/chart",
  },
  {
    id: "code-block",
    label: "Code Block",
    description: "Display syntax-highlighted code snippets",
    path: "/docs/code-block",
  },
  {
    id: "data-table",
    label: "Data Table",
    description: "Present structured data in sortable tables",
    path: "/docs/data-table",
  },
  {
    id: "image",
    label: "Image",
    description: "Display images with metadata and attribution",
    path: "/docs/image",
  },
  {
    id: "video",
    label: "Video",
    description: "Video playback with controls and poster",
    path: "/docs/video",
  },
  {
    id: "audio",
    label: "Audio",
    description: "Audio playback with artwork and metadata",
    path: "/docs/audio",
  },
  {
    id: "link-preview",
    label: "Link Preview",
    description: "Rich link previews with OG data",
    path: "/docs/link-preview",
  },
  {
    id: "option-list",
    label: "Option List",
    description: "Let users select from multiple choices",
    path: "/docs/option-list",
  },
  {
    id: "plan",
    label: "Plan",
    description: "Display step-by-step task workflows",
    path: "/docs/plan",
  },
  {
    id: "item-carousel",
    label: "Item Carousel",
    description: "Horizontal carousel for browsing collections",
    path: "/docs/item-carousel",
  },
  {
    id: "social-post",
    label: "Social Posts",
    description: "Render social media content previews",
    path: "/docs/social-post",
  },
  {
    id: "terminal",
    label: "Terminal",
    description: "Show command-line output and logs",
    path: "/docs/terminal",
  },
];

export function getComponentById(id: string): ComponentMeta | undefined {
  return componentsRegistry.find((component) => component.id === id);
}


--- app/docs/advanced/content.mdx ---
import { DocsHeader } from "../_components/docs-header";

<DocsHeader
  title="Advanced"
  description="Deep-dive topics for stronger typing and larger integrations."
  mdxPath="app/docs/advanced/content.mdx"
/>

## Tool Type Inference

With `InferUITools` in AI SDK 5, you can infer tool input/output types from your tool set and get fully typed `message.parts` in the UI.

- [**InferUITools reference**](https://ai-sdk.dev/docs/reference/ai-sdk-ui/infer-ui-tools)

<Steps>

<Step>

### Define Tools with Output Schemas

```ts title="lib/tools.ts"
import { tool } from "ai";
import { z } from "zod";
import { SerializableLinkPreviewSchema } from "@/components/tool-ui/link-preview/schema";

export const tools = {
  previewLink: tool({
    description: "Return a simple link preview",
    inputSchema: z.object({ url: z.url() }),
    outputSchema: SerializableLinkPreviewSchema,
    async execute({ url }) {
      return {
        id: "link-preview-1",
        href: url,
        title: "React Server Components",
        image:
          "https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=1200",
        domain: new URL(url).hostname,
      };
    },
  }),
} as const;

// Export a type only; the client should import types, not server code.
export type Tools = typeof tools;
```

</Step>

<Step>

### Use Tools on the Server

```ts
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { tools } from "@/lib/tools";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-5-nano"),
    messages: convertToModelMessages(messages),
    tools,
  });

  return result.toUIMessageStreamResponse();
}
```

</Step>

<Step>

### Infer UI-Level Types in the Client

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { InferUITools } from "ai";
import type { Tools } from "@/lib/tools";
import {
  LinkPreview,
  parseSerializableLinkPreview,
} from "@/components/tool-ui/link-preview";

type MyUITools = InferUITools<Tools>;

export default function Chat() {
  const { messages } = useChat<MyUITools>({ api: "/api/chat" });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          {m.parts.map((part, i) => {
            // Fully typed: 'tool-previewLink' with correct output shape
            if (
              part.type === "tool-previewLink" &&
              part.state === "output-available"
            ) {
              const preview = parseSerializableLinkPreview(part.output);
              return <LinkPreview key={i} {...preview} />;
            }
            return null;
          })}
        </div>
      ))}
    </div>
  );
}
```

</Step>

</Steps>

## Tips

### Keep runtime boundaries clean

Export only types from `lib/tools.ts` to the client; never import server code into the browser.

### Single-tool helpers

For a single tool, `InferUITool` works the same way as `InferUITools` but for one tool definition.

### End-to-end validation

Use the component's `outputSchema` on the server and `parseSerializable{componentName}` on the client to validate at both ends.

### Lifecycle-aware rendering

Use `part.state` to respect the Tool UI lifecycle from UI Guidelines:

- `state === 'invocation'` → show intent / shell
- `state === 'output-pending'` → show skeleton / loading shell
- `state === 'output-available'` → render the full component
- `state === 'errored'` → show an error surface instead of silently failing


## Links discovered
- [**InferUITools reference**](https://ai-sdk.dev/docs/reference/ai-sdk-ui/infer-ui-tools)

--- app/docs/audio/content.mdx ---
import { DocsHeader } from "../_components/docs-header";
import { Audio } from "@/components/tool-ui/audio";

<DocsHeader
  title="Audio"
  description="Audio playback with artwork and metadata."
  mdxPath="app/docs/audio/content.mdx"
/>

<Tabs items={["Preview", "Code"]}>
  <Tab>
    <div className="not-prose mx-auto max-w-sm">
      <Audio
        id="audio-example"
        assetId="sample-audio"
        src="https://samplelib.com/lib/preview/mp3/sample-6s.mp3"
        title="Bell Labs hallway recording"
        description="Ambient sounds where UNIX, C, and more took shape."
        artwork="https://images.unsplash.com/photo-1454165205744-3b78555e5572?w=400&auto=format&fit=crop"
        durationMs={30000}
      />
    </div>
  </Tab>
  <Tab>
    ```tsx
    import { Audio } from "@/components/tool-ui/audio";

    export function Example() {
      return (
        <Audio
          id="audio-example"
          assetId="sample-audio"
          src="https://samplelib.com/lib/preview/mp3/sample-6s.mp3"
          title="Bell Labs hallway recording"
          description="Ambient sounds where UNIX, C, and more took shape."
          artwork="https://images.unsplash.com/photo-1454165205744-3b78555e5572"
          durationMs={30000}
        />
      );
    }
    ```
  </Tab>
</Tabs>

## Key Features

<FeatureGrid>
  <Feature icon="Headphones" title="Native audio controls">
    Play, pause, seek, and volume with browser controls
  </Feature>
  <Feature icon="Image" title="Artwork display">
    Album art or thumbnail alongside the player
  </Feature>
</FeatureGrid>

## When to Use

- **Good for:** Audio clips, podcasts, music previews, voice recordings
- **Not for:** Video content (use [Video](/docs/video)), image content (use [Image](/docs/image))

## Source and Install

Copy [`components/tool-ui/audio`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/audio) and the [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) directory into your project.

### Prerequisites

```bash
pnpm dlx shadcn@latest add badge button card tooltip
```

### Download

- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Audio (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/audio)

## Props

<TypeTable
  type={{
    id: {
      description: "Unique identifier for this tool UI instance",
      type: "string",
      required: true,
    },
    assetId: {
      description: "Persistent asset identifier",
      type: "string",
      required: true,
    },
    src: {
      description: "Audio source URL",
      type: "string",
      required: true,
    },
    title: { description: "Title text", type: "string" },
    description: { description: "Description", type: "string" },
    artwork: { description: "Artwork/thumbnail URL", type: "string" },
    durationMs: { description: "Duration in milliseconds", type: "number" },
    fileSizeBytes: { description: "File size in bytes", type: "number" },
    source: {
      description: "Source attribution",
      type: "{ label: string; iconUrl?: string; url?: string }",
    },
    createdAt: { description: "Creation timestamp (ISO 8601)", type: "string" },
    responseActions: { description: "Response action buttons", type: "Action[]" },
    onResponseAction: { description: "Response action handler", type: "(actionId: string) => void" },
  }}
/>


## Links discovered
- [Video](https://github.com/assistant-ui/tool-ui/blob/main/docs/video.md)
- [Image](https://github.com/assistant-ui/tool-ui/blob/main/docs/image.md)
- [`components/tool-ui/audio`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/audio)
- [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Audio (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/audio)

--- app/docs/chart/content.mdx ---
import { DocsHeader } from "../_components/docs-header";

<DocsHeader
  title="Chart"
  description="Display interactive data visualizations."
  mdxPath="app/docs/chart/content.mdx"
/>

<ChartPresetExample preset="revenue" />

## Key Features

<FeatureGrid>
  <Feature icon="BarChart3" title="Bar and line charts">
    Choose the right visualization for your data
  </Feature>
  <Feature icon="Layers" title="Multiple series">
    Compare datasets side-by-side with automatic coloring
  </Feature>
  <Feature icon="MousePointerClick" title="Interactive tooltips">
    Click handlers and hover tooltips for deeper exploration
  </Feature>
  <Feature icon="Settings2" title="Configurable display">
    Toggle legends, grid lines, and custom color palettes
  </Feature>
</FeatureGrid>

## When to Use

- **Good for:** Trends, comparisons, distributions, time-series data
- **Not for:** Single values (use text), detailed tabular data (use [Data Table](/docs/data-table)), complex dashboards

## Variants

### Line Charts

Switch to line charts for time-series or continuous data.

<ChartPresetExample preset="performance" />

### Minimal Configuration

Charts work with just `data`, `xKey`, and `series`. Title, description, legend, and grid are optional.

<ChartPresetExample preset="minimal" />

## Source and Install

Copy [`components/tool-ui/chart`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart) and the [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) directory into your project. The `shared` folder contains utilities used by all Tool UI components. The `tool-ui` directory should sit alongside your shadcn `ui` directory.

### Prerequisites

This component requires the following [shadcn/ui](https://ui.shadcn.com) components:

```bash
pnpm dlx shadcn@latest add card chart
```

### Directory Structure

<Files>
  <Folder name="components" defaultOpen>
    <Folder name="ui">
      <File name="chart.tsx" />
      <File name="card.tsx" />
      <File name="..." />
    </Folder>
    <Folder name="tool-ui" defaultOpen>
      <Folder name="shared" defaultOpen>
        <File name="action-buttons.tsx" />
        <File name="schema.ts" />
        <File name="index.ts" />
        <File name="..." />
      </Folder>
      <Folder name="chart" defaultOpen>
        <File name="chart.tsx" />
        <File name="schema.ts" />
        <File name="index.tsx" />
        <File name="..." />
      </Folder>
    </Folder>
  </Folder>
</Files>

### Download

- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared))
- [**Chart (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart))

## Usage

```tsx
import { Chart } from "@/components/tool-ui/chart";

<Chart
  id="monthly-revenue"
  type="bar"
  title="Monthly Revenue"
  description="2024 YTD"
  data={[
    { month: "Jan", revenue: 4000, expenses: 2400 },
    { month: "Feb", revenue: 3000, expenses: 1398 },
    // ...
  ]}
  xKey="month"
  series={[
    { key: "revenue", label: "Revenue" },
    { key: "expenses", label: "Expenses" },
  ]}
  showLegend
  showGrid
/>;
```

### With assistant-ui (tool output)

Define a tool that returns chart data, then render the component with runtime validation.

```tsx
// Backend tool
import { tool, jsonSchema } from "ai";

const showChart = tool({
  description: "Display data as a chart",
  inputSchema: jsonSchema<{ metric: string }>({
    type: "object",
    properties: { metric: { type: "string" } },
    required: ["metric"],
    additionalProperties: false,
  }),
  async execute({ metric }) {
    const data = await fetchMetricData(metric);
    return {
      id: `chart-${metric}`,
      type: "line",
      title: `${metric} Over Time`,
      data,
      xKey: "date",
      series: [{ key: "value", label: metric }],
      showGrid: true,
    };
  },
});

// Frontend with assistant-ui
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
  Chart,
  ChartErrorBoundary,
  parseSerializableChart,
} from "@/components/tool-ui/chart";

export const ShowChartUI = makeAssistantToolUI({
  toolName: "showChart",
  render: ({ result }) => {
    if (result === undefined) {
      return (
        <div className="bg-card/60 text-muted-foreground w-full max-w-xl rounded-2xl border px-5 py-4 text-sm shadow-xs">
          Loading chart…
        </div>
      );
    }

    const chart = parseSerializableChart(result);
    return (
      <ChartErrorBoundary>
        <Chart {...chart} />
      </ChartErrorBoundary>
    );
  },
});
```

### With Custom Colors

```tsx
<Chart
  id="custom-colors"
  type="bar"
  data={data}
  xKey="month"
  series={[
    { key: "revenue", label: "Revenue" },
    { key: "expenses", label: "Expenses" },
  ]}
  colors={["#22c55e", "#ef4444"]}
/>
```

### With Click Handler

```tsx
<Chart
  id="clickable"
  type="line"
  data={data}
  xKey="time"
  series={[{ key: "value", label: "Value" }]}
  onDataPointClick={(point) => {
    console.log("Clicked:", point.xValue, point.yValue);
  }}
/>
```

## Props

<TypeTable
  type={{
    id: {
      description: "Unique identifier for this chart instance",
      type: "string",
      required: true,
    },
    type: {
      description: "Chart type",
      type: "'bar' | 'line'",
      required: true,
    },
    data: {
      description: "Array of data records",
      type: "Record<string, unknown>[]",
      required: true,
    },
    xKey: {
      description: "Key for x-axis values",
      type: "string",
      required: true,
    },
    series: {
      description: "Data series to plot",
      type: "ChartSeries[]",
      required: true,
    },
    title: {
      description: "Chart title (renders in Card header)",
      type: "string",
    },
    description: {
      description: "Chart description (renders below title)",
      type: "string",
    },
    colors: {
      description: "Custom color palette for series",
      type: "string[]",
    },
    showLegend: {
      description: "Show legend below chart",
      type: "boolean",
      default: "false",
    },
    showGrid: {
      description: "Show background grid lines",
      type: "boolean",
      default: "true",
    },
    onDataPointClick: {
      description: "Click handler for data points",
      type: "(point: ChartDataPoint) => void",
    },
  }}
/>

## Series Schema

<TypeTable
  type={{
    key: {
      description: "Data key to plot",
      type: "string",
      required: true,
    },
    label: {
      description: "Display label for legend/tooltip",
      type: "string",
      required: true,
    },
    color: {
      description: "Override color for this series",
      type: "string",
    },
  }}
/>

## ChartDataPoint (click handler payload)

<TypeTable
  type={{
    seriesKey: {
      description: "Key of the clicked series",
      type: "string",
    },
    seriesLabel: {
      description: "Label of the clicked series",
      type: "string",
    },
    xValue: {
      description: "X-axis value at click point",
      type: "unknown",
    },
    yValue: {
      description: "Y-axis value at click point",
      type: "unknown",
    },
    index: {
      description: "Index in data array",
      type: "number",
    },
    payload: {
      description: "Full data record for clicked point",
      type: "Record<string, unknown>",
    },
  }}
/>

## Accessibility

- Built on Recharts with semantic SVG structure
- Tooltips and legends provide text alternatives for visual data
- Inherits focus management from shadcn/ui Card primitives


## Links discovered
- [Data Table](https://github.com/assistant-ui/tool-ui/blob/main/docs/data-table.md)
- [`components/tool-ui/chart`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart)
- [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [shadcn/ui](https://ui.shadcn.com)
- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Chart (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/chart)

--- app/docs/code-block/content.mdx ---
import { DocsHeader } from "../_components/docs-header";

<DocsHeader
  title="Code Block"
  description="Display syntax-highlighted code."
  mdxPath="app/docs/code-block/content.mdx"
/>

<CodeBlockPresetExample preset="typescript" />

## Key Features

<FeatureGrid>
  <Feature icon="Palette" title="Syntax highlighting">
    Powered by Shiki with support for 100+ languages
  </Feature>
  <Feature icon="Highlighter" title="Line highlighting">
    Draw attention to specific lines for explanations or bug indicators
  </Feature>
  <Feature icon="FoldVertical" title="Collapsible mode">
    Auto-collapse long snippets to keep conversations scannable
  </Feature>
  <Feature icon="FileCode" title="Filename header">
    Show filename and language badge for context
  </Feature>
</FeatureGrid>

## When to Use

- **Good for:** Code examples, configuration files, API responses, scripts
- **Not for:** Interactive code editing (use [CodeMirror](https://codemirror.net/)), large files (consider linking)

## Source and Install

Copy [`components/tool-ui/code-block`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block) and the [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) directory into your project. The `shared` folder contains utilities used by all Tool UI components. The `tool-ui` directory should sit alongside your shadcn `ui` directory.

### Prerequisites

This component requires the following [shadcn/ui](https://ui.shadcn.com) components:

```bash
pnpm dlx shadcn@latest add button
```

It also uses [Radix Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible) directly:

```bash
pnpm add @radix-ui/react-collapsible
```

### Directory Structure

<Files>
  <Folder name="components" defaultOpen>
    <Folder name="ui">
      <File name="button.tsx" />
      <File name="..." />
    </Folder>
    <Folder name="tool-ui" defaultOpen>
      <Folder name="shared" defaultOpen>
        <File name="action-buttons.tsx" />
        <File name="schema.ts" />
        <File name="index.ts" />
        <File name="..." />
      </Folder>
      <Folder name="code-block" defaultOpen>
        <File name="code-block.tsx" />
        <File name="schema.ts" />
        <File name="index.tsx" />
        <File name="progress.tsx" />
        <File name="_adapter.tsx" />
      </Folder>
    </Folder>
  </Folder>
</Files>

### Download

- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared))
- [**Code Block (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block))

Install dependencies ([Shiki](https://shiki.style/) for syntax highlighting):

```bash
pnpm add shiki
```

## Usage

```tsx
import { CodeBlock } from "@/components/tool-ui/code-block";

export function MyComponent() {
  return (
    <CodeBlock
      id="my-code-example"
      code={`function hello(name: string) {
  return \`Hello, \${name}!\`;
}`}
      language="typescript"
      filename="hello.ts"
      showLineNumbers={true}
    />
  );
}
```

### With assistant-ui (tool output)

Define a tool that returns Code Block props, then render the component with runtime validation.

```tsx
// Backend tool
import { tool, jsonSchema } from "ai";

const showCodeBlock = tool({
  description: "Show a code snippet",
  inputSchema: jsonSchema<{}>({
    type: "object",
    properties: {},
    additionalProperties: false,
  }),
  async execute() {
    return {
      id: "code-block-1",
      code: "console.log('hello');",
      language: "javascript",
      filename: "hello.js",
      highlightLines: [1],
    };
  },
});

// Frontend with assistant-ui
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
  CodeBlock,
  CodeBlockErrorBoundary,
  parseSerializableCodeBlock,
} from "@/components/tool-ui/code-block";

function ParsedCodeBlock({ result }: { result: unknown }) {
  const props = parseSerializableCodeBlock(result);
  return <CodeBlock {...props} />;
}

export const ShowCodeBlockUI = makeAssistantToolUI({
  toolName: "showCodeBlock",
  render: ({ result }) => {
    // Tool outputs stream in; `result` will be `undefined` until the tool resolves.
    if (result === undefined) {
      return <CodeBlock id="loading" code="" language="text" isLoading />;
    }

    return (
      <CodeBlockErrorBoundary>
        <ParsedCodeBlock result={result} />
      </CodeBlockErrorBoundary>
    );
  },
});
```

## Props

### Core Props

<TypeTable
  type={{
    code: {
      description: "The code content to display",
      type: "string",
      required: true,
    },
    language: {
      description: "Programming language for syntax highlighting",
      type: "string",
      default: '"text"',
    },
    filename: {
      description: "Filename to display in the header",
      type: "string",
    },
  }}
/>

### Display Options

<TypeTable
  type={{
    showLineNumbers: {
      description: "Show line numbers",
      type: "boolean",
      default: "true",
    },
    highlightLines: {
      description: "Array of line numbers to highlight",
      type: "number[]",
    },
    maxCollapsedLines: {
      description: "Auto-collapse if code exceeds this many lines",
      type: "number",
    },
  }}
/>

### Standard Props

<TypeTable
  type={{
    id: {
      description: "Unique identifier for this component",
      type: "string",
      required: true,
    },
    responseActions: {
      description: "Response action buttons",
      type: "Action[]",
    },
    className: {
      description: "Additional CSS classes",
      type: "string",
    },
    isLoading: {
      description: "Show loading skeleton state",
      type: "boolean",
    },
  }}
/>

## Supported Languages

CodeBlock supports all languages included with Shiki, including:

- TypeScript/JavaScript
- Python
- JSON
- Bash/Shell
- CSS/HTML
- Markdown
- SQL
- YAML
- Go
- Rust
- And many more...

See [Shiki documentation](https://shiki.style/languages) for the complete list.

## Accessibility

- Collapsible sections use Radix Collapsible with proper ARIA attributes
- Copy button is keyboard-accessible with focus indication
- Code content is selectable and works with screen readers


## Links discovered
- [CodeMirror](https://codemirror.net/)
- [`components/tool-ui/code-block`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block)
- [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [shadcn/ui](https://ui.shadcn.com)
- [Radix Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible)
- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Code Block (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/code-block)
- [Shiki](https://shiki.style/)
- [Shiki documentation](https://shiki.style/languages)

--- app/docs/contributing/content.mdx ---
import { DocsHeader } from "../_components/docs-header";

<DocsHeader
  title="Contributing"
  description="Guidelines for contributing new components to Tool UI."
  mdxPath="app/docs/contributing/content.mdx"
/>

This page is for people working **inside this repo**. It covers building or modifying components, not app integration.

## Component Structure

Each component lives in [`components/tool-ui/`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui) with this file organization:

### Core Files

- `schema.ts`: Serializable schema and parser (`serializableXSchema`, `parseSerializableX`)
- `types.ts`: TypeScript type definitions (serializable props, client props, internal types)
- `component-name.tsx`: Main component implementation
- `index.tsx`: Public exports (component, types, parser, error boundary)
- `error-boundary.tsx`: Error boundary wrapper for the component

### Sub-Components (as needed)

- `header.tsx`, `body.tsx`, `footer.tsx`, `actions.tsx`: Smaller composable pieces
- `context.tsx`: React context for shared state within the component

### Utilities

- `_adapter.tsx`: Re-exports `cn` utility and shadcn/ui primitives (adapter pattern for portability)
- `formatters.tsx`: Data formatting helpers (if needed)
- `utilities.ts`: Component-specific helper functions

### Documentation

- `README.md`: Component overview, features, installation, API reference

## Implementation Checklist

<Steps>

<Step>

### Define the Contract

Before coding:

- **Intent**: One-sentence purpose in a chat context (for example, "Inline approval prompt for destructive actions.")
- **Role**: One of: information, decision, control, observability, or composite (see [UI Guidelines](/docs/design-guidelines))
- **Serializable schema**: Design the Zod schema with JSON-serializable props only (no functions, Dates, class instances)
- **Required vs optional fields**: Which props are essential? Which have safe defaults?
- **States**: How does it handle loading, empty, error, interactive, and receipt states?
- **Addressability**: How are rows, actions, and sub-elements identified so the assistant can refer to them in later turns?
- **Actions**: What actions can users take, and what parameters do we send back to the app?

</Step>

<Step>

### Build the Component

Implementation pattern:

1. `schema.ts`: Define `serializableXSchema` and `parseSerializableX(input: unknown)`
2. `types.ts`: Create:
   - Serializable props type (derived from the schema)
   - Client props type (serializable props + callbacks like `onAction`, `onBeforeAction`)
3. `_adapter.tsx`: Re-export `cn` and the shadcn/ui primitives you need (Button, Card, etc.)
4. `component-name.tsx`: Build the main component, focused on:
   - Single intent and role
   - Lifecycle states from [UI Guidelines](/docs/design-guidelines)
   - Minimal but clear chrome
5. Sub-components: Extract header/body/footer/actions when the component grows
6. `context.tsx`: Use React context only when sub-components genuinely need shared state
7. `error-boundary.tsx`: Wrap the component so bad props don't crash the whole chat
8. `index.tsx`: Export the public API (component, types, parser, error boundary)

</Step>

<Step>

### Document Usage

Create clear documentation:

- **README.md**: Features, installation instructions, basic usage, props table
- Include at least one example that shows:
  - Tool definition with `outputSchema`
  - Server-side execution returning serializable props
  - Client-side `parseSerializableX` + `<Component />` usage
- Document action handling and any receipt behavior

</Step>

<Step>

### Verify Quality

Ensure the component meets standards:

- **Accessibility**: WCAG AA contrast, usable touch targets, keyboard navigation, screen reader labels
- **Responsive**: Works on mobile (320px) through desktop widths
- **No scroll traps**: Avoid inner scroll areas when possible; let the chat container handle scrolling
- **Reduced motion**: Respect `prefers-reduced-motion` for animations
- **Error handling**: Validate props, show graceful error states, use error boundaries
- **Performance**: Avoid unnecessary re-renders and layout thrash

</Step>

</Steps>

## Design Principles for Contributors

All components must follow the [UI Guidelines](/docs/design-guidelines). In practice, that means:

- **Single intent**: One primary job per component instance
- **Clear role**: Information, decision, control, or observability (composites must name a primary role)
- **Conversation-first**: Compact, glanceable, and readable in a few seconds
- **Receipt-aware**: Any action with side effects must have a receipt pattern
- **Schema-driven**: Props must be derivable from a serializable schema
- **Serializable vs client-only**: Keep serializable props separate from client-only props (callbacks, ReactNodes, and so on), with clear naming between the two
- **Accessible by default**: Keyboard, screen reader, and contrast-friendly out of the box

Use UI Guidelines for philosophy; this page is about how to express that philosophy in code.

## Submitting Components

When you are ready to contribute:

1. **Test thoroughly**: Check across viewports, themes, and basic assistive tech (screen reader, keyboard only)
2. **Document completely**: README with features, API, and at least one end-to-end example
3. **Follow conventions**: Match existing component patterns and file structure
4. **Open a PR**: Include screenshots or screen recordings, and link to the relevant docs section

Questions? [Open an issue on GitHub](https://github.com/assistant-ui/tool-ui/issues) or [reach out on Discord](https://discord.com/channels/1251324227668283443).


## Links discovered
- [`components/tool-ui/`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui)
- [UI Guidelines](https://github.com/assistant-ui/tool-ui/blob/main/docs/design-guidelines.md)
- [Open an issue on GitHub](https://github.com/assistant-ui/tool-ui/issues)
- [reach out on Discord](https://discord.com/channels/1251324227668283443)

--- app/docs/data-table/content.mdx ---
import { DocsHeader } from "../_components/docs-header";
import { DataTable } from "@/components/tool-ui/data-table";
import { FormatInlineExample } from "./formatting-gallery";
import {
  NumberValue,
  CurrencyValue,
  PercentValue,
  DeltaValue,
  DateValue,
  BooleanValue,
  LinkValue,
  BadgeValue,
  StatusBadge,
  ArrayValue,
} from "@/components/tool-ui/data-table";

<DocsHeader
  title="Data Table"
  description="Display tabular data."
  mdxPath="app/docs/data-table/content.mdx"
/>

<Tabs items={["Preview", "Code"]}>
  <Tab>
    <div className="not-prose">
      <DataTable
        rowIdKey="id"
        defaultSort={{ by: "createdAt", direction: "desc" }}
        columns={[
          { key: "issue", label: "Issue", priority: "primary", truncate: true },
          {
            key: "priority",
            label: "Urgency",
            format: {
              kind: "status",
              statusMap: {
                high: { tone: "danger", label: "High" },
                medium: { tone: "warning", label: "Medium" },
                low: { tone: "neutral", label: "Low" },
              },
            },
          },
          {
            key: "createdAt",
            label: "Created",
            format: { kind: "date", dateFormat: "relative" },
          },
          {
            key: "amount",
            label: "Amount",
            align: "right",
            format: { kind: "currency", currency: "USD", decimals: 2 },
          },
        ]}
        data={[
          {
            id: "1",
            issue: "Payment failing on checkout",
            priority: "high",
            createdAt: "2025-11-11T09:24:00Z",
            amount: 129.99,
          },
          {
            id: "2",
            issue: "Billing UI alignment bug",
            priority: "medium",
            createdAt: "2025-11-10T17:03:00Z",
            amount: 42,
          },
          {
            id: "3",
            issue: "Export CSV missing headers",
            priority: "low",
            createdAt: "2025-11-08T08:00:00Z",
            amount: 0,
          },
          {
            id: "4",
            issue: "Webhook retries inconsistent",
            priority: "high",
            createdAt: "2025-11-13T22:15:00Z",
            amount: 527.5,
          },
        ]}
      />
    </div>
  </Tab>
  <Tab>
    ```tsx
    import { DataTable, type Column } from "@/components/tool-ui/data-table";

    type Row = {
      id: string;
      issue: string;
      priority: "high" | "medium" | "low";
      createdAt: string;
      amount: number;
    };

    const columns: Column<Row>[] = [
      { key: "issue", label: "Issue", priority: "primary", truncate: true },
      {
        key: "priority",
        label: "Urgency",
        format: {
          kind: "status",
          statusMap: {
            high: { tone: "danger", label: "High" },
            medium: { tone: "warning", label: "Medium" },
            low: { tone: "neutral", label: "Low" },
          },
        },
      },
      { key: "createdAt", label: "Created", format: { kind: "date", dateFormat: "relative" } },
      { key: "amount", label: "Amount", align: "right", format: { kind: "currency", currency: "USD", decimals: 2 } },
    ];

    const data: Row[] = [
      {
        id: "1",
        issue: "Payment failing on checkout",
        priority: "high",
        createdAt: "2025-11-11T09:24:00Z",
        amount: 129.99,
      },
      {
        id: "2",
        issue: "Billing UI alignment bug",
        priority: "medium",
        createdAt: "2025-11-10T17:03:00Z",
        amount: 42,
      },
      {
        id: "3",
        issue: "Export CSV missing headers",
        priority: "low",
        createdAt: "2025-11-08T08:00:00Z",
        amount: 0,
      },
      {
        id: "4",
        issue: "Webhook retries inconsistent",
        priority: "high",
        createdAt: "2025-11-13T22:15:00Z",
        amount: 527.5,
      },
    ];

    export default function Example() {
      return (
        <DataTable
          rowIdKey="id"
          columns={columns}
          data={data}
          defaultSort={{ by: "createdAt", direction: "desc" }}
        />
      );
    }
    ```

  </Tab>
</Tabs>

## Key Features

<FeatureGrid>
  <Feature icon="Smartphone" title="Responsive layout">
    Automatically switches from table to cards on mobile
  </Feature>
  <Feature icon="ArrowUpDown" title="Sorting">
    Tri-state column sorting: none → ascending → descending
  </Feature>
  <Feature icon="Table2" title="Declarative formatting">
    Currency, dates, links, badges, and more via config
  </Feature>
  <Feature icon="MousePointerClick" title="Response actions">
    Add actionable buttons below the table
  </Feature>
</FeatureGrid>

## When to Use

- **Good for:** Search results, lists, dashboards, logs with sorting
- **Not for:** Single-record details (use [Image](/docs/image), [Video](/docs/video), or [Link Preview](/docs/link-preview)), simple choice lists (use [Option List](/docs/option-list)), complex multi-step forms

## Source and Install

Copy [`components/tool-ui/data-table`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table) and the [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) directory into your project. The `shared` folder contains utilities used by all Tool UI components. The `tool-ui` directory should sit alongside your shadcn `ui` directory.

### Prerequisites

This component requires the following [shadcn/ui](https://ui.shadcn.com) components:

```bash
pnpm dlx shadcn@latest add accordion badge button dropdown-menu table tooltip
```

### Directory Structure

<Files>
  <Folder name="components" defaultOpen>
    <Folder name="ui">
      <File name="accordion.tsx" />
      <File name="badge.tsx" />
      <File name="button.tsx" />
      <File name="dropdown-menu.tsx" />
      <File name="table.tsx" />
      <File name="tooltip.tsx" />
      <File name="..." />
    </Folder>
    <Folder name="tool-ui" defaultOpen>
      <Folder name="shared" defaultOpen>
        <File name="action-buttons.tsx" />
        <File name="schema.ts" />
        <File name="index.ts" />
        <File name="..." />
      </Folder>
      <Folder name="data-table" defaultOpen>
        <File name="data-table.tsx" />
        <File name="schema.ts" />
        <File name="types.ts" />
        <File name="index.tsx" />
        <File name="..." />
      </Folder>
    </Folder>
  </Folder>
</Files>

### Download

- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared))
- [**Data Table (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table) ([ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table))

## Usage

Define a tool that returns table data, then render with the DataTable component.

```tsx
// Backend tool
import { tool, jsonSchema } from "ai";

const searchProducts = tool({
  description: "Search products and return results in a table",
  inputSchema: jsonSchema<{ query: string }>({
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"],
    additionalProperties: false,
  }),
  async execute({ query }) {
    const results = await db.products.search(query);
    return {
      id: "data-table-search-products",
      columns: [
        { key: "name", label: "Product", priority: "primary" },
        {
          key: "price",
          label: "Price",
          format: { kind: "currency", currency: "USD" },
        },
        { key: "stock", label: "Stock", align: "right" },
      ],
      data: results.map((p) => ({
        id: p.id,
        name: p.name,
        price: p.price,
        stock: p.stock,
      })),
    };
  },
});

// Frontend with assistant-ui
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
  DataTable,
  DataTableErrorBoundary,
  parseSerializableDataTable,
} from "@/components/tool-ui/data-table";

export const SearchProductsUI = makeAssistantToolUI({
  toolName: "searchProducts",
  render: ({ result }) => {
    // Tool outputs stream in; `result` will be `undefined` until the tool resolves.
    if (result === undefined) {
      return (
        <div className="bg-card/60 text-muted-foreground w-full max-w-xl rounded-2xl border px-5 py-4 text-sm shadow-xs">
          Loading table…
        </div>
      );
    }

    const props = parseSerializableDataTable(result);
    return (
      <DataTableErrorBoundary>
        <DataTable rowIdKey="id" {...props} />
      </DataTableErrorBoundary>
    );
  },
});
```

## Props

### Core Props

<TypeTable
  type={{
    columns: {
      description: "Column definitions",
      type: "Column[]",
      required: true,
    },
    data: {
      description: "Rows (JSON primitives only)",
      type: "T[]",
      required: true,
    },
    rowIdKey: {
      description: "Unique key per row",
      type: "keyof T",
      required: true,
    },
    locale: { description: "Intl locale", type: "string", default: '"en-US"' },
  }}
/>

### Column Options

<TypeTable
  type={{
    key: { description: "Row data key", type: "keyof T", required: true },
    label: { description: "Header text", type: "string", required: true },
    format: { description: "Cell formatting", type: "FormatConfig" },
    priority: {
      description: "Mobile visibility",
      type: "'primary' | 'secondary' | 'tertiary'",
    },
    sortable: {
      description: "Enable sorting",
      type: "boolean",
      default: "true",
    },
    align: {
      description: "Text alignment",
      type: "'left' | 'right' | 'center'",
    },
    truncate: { description: "Truncate with ellipsis", type: "boolean" },
  }}
/>

### Sorting

<TypeTable
  type={{
    defaultSort: {
      description: "Initial sort",
      type: "{ by?: keyof T; direction?: 'asc' | 'desc' }",
    },
    sort: {
      description: "Controlled sort",
      type: "{ by?: keyof T; direction?: 'asc' | 'desc' }",
    },
    onSortChange: { description: "Sort handler", type: "(sort) => void" },
  }}
/>

### Response Actions

<TypeTable
  type={{
    responseActions: {
      description: "Surface-level actions below the table",
      type: "Action[] | ActionsConfig",
    },
    onResponseAction: {
      description: "Click handler",
      type: "(actionId: string) => void",
    },
    onBeforeResponseAction: {
      description: "Gate actions (return false to cancel)",
      type: "(actionId: string) => boolean | Promise<boolean>",
    },
  }}
/>

## Formatting

All visual formatting for data stays declarative via `columns[i].format`. Keep row values as primitives and describe presentation through format configs.

<TypeTable
  type={{
    number: {
      description: (
        <FormatInlineExample
          value={12345.678}
          output={
            <NumberValue
              value={12345.678}
              options={{ kind: "number", decimals: 2, unit: " ms" }}
            />
          }
        />
      ),
      type: "{ kind: 'number', decimals?: number, unit?: string }",
    },
    currency: {
      description: (
        <FormatInlineExample
          value={178.25}
          output={
            <CurrencyValue
              value={178.25}
              options={{ kind: "currency", currency: "USD", decimals: 2 }}
            />
          }
        />
      ),
      type: "{ kind: 'currency', currency: string, decimals?: number }",
    },
    percent: {
      description: (
        <>
          <FormatInlineExample
            value={0.123}
            output={
              <PercentValue
                value={0.123}
                options={{
                  kind: "percent",
                  basis: "fraction",
                  decimals: 1,
                  showSign: true,
                }}
              />
            }
          />
          <FormatInlineExample
            value={1.23}
            output={
              <PercentValue
                value={1.23}
                options={{
                  kind: "percent",
                  basis: "unit",
                  decimals: 2,
                  showSign: true,
                }}
              />
            }
          />
        </>
      ),
      type: "{ kind: 'percent', basis: 'fraction' | 'unit', decimals?: number, showSign?: boolean }",
    },
    delta: {
      description: (
        <>
          <FormatInlineExample
            value={2.35}
            output={
              <DeltaValue
                value={2.35}
                options={{
                  kind: "delta",
                  decimals: 2,
                  upIsPositive: true,
                  showSign: true,
                }}
              />
            }
          />
          <FormatInlineExample
            value={-1.2}
            output={
              <DeltaValue
                value={-1.2}
                options={{
                  kind: "delta",
                  decimals: 2,
                  upIsPositive: true,
                  showSign: true,
                }}
              />
            }
          />
        </>
      ),
      type: "{ kind: 'delta', decimals?: number, upIsPositive?: boolean, showSign?: boolean }",
    },
    date: {
      description: (
        <>
          <FormatInlineExample
            value={"2025-01-05T12:00:00Z"}
            output={
              <DateValue
                value={"2025-01-05T12:00:00Z"}
                options={{ kind: "date", dateFormat: "relative" }}
              />
            }
          />
          <FormatInlineExample
            value={"2025-01-05T12:00:00Z"}
            output={
              <DateValue
                value={"2025-01-05T12:00:00Z"}
                options={{ kind: "date", dateFormat: "long" }}
                locale="de-DE"
              />
            }
            note="Long date with locale"
          />
        </>
      ),
      type: "{ kind: 'date', dateFormat?: 'short' | 'long' | 'relative' }",
    },
    boolean: {
      description: (
        <FormatInlineExample
          value={true}
          output={
            <BooleanValue
              value={true}
              options={{
                kind: "boolean",
                labels: { true: "Yes", false: "No" },
              }}
            />
          }
        />
      ),
      type: "{ kind: 'boolean', labels?: { true: string; false: string } }",
    },
    link: {
      description: (
        <>
          <FormatInlineExample
            value={"Docs Portal"}
            output={
              <LinkValue
                value={"Docs Portal"}
                row={{ url: "/docs" }}
                options={{ kind: "link", hrefKey: "url" }}
              />
            }
          />
          <FormatInlineExample
            value={"https://example.com"}
            output={
              <LinkValue
                value={"https://example.com"}
                options={{ kind: "link", external: true }}
              />
            }
          />
        </>
      ),
      type: "{ kind: 'link', hrefKey?: string, external?: boolean }",
    },
    badge: {
      description: (
        <FormatInlineExample
          value={"open"}
          output={
            <BadgeValue
              value={"open"}
              options={{
                kind: "badge",
                colorMap: { open: "info", closed: "success" },
              }}
            />
          }
        />
      ),
      type: "{ kind: 'badge', colorMap?: Record<string, Tone> }",
    },
    status: {
      description: (
        <FormatInlineExample
          value={"high"}
          output={
            <StatusBadge
              value={"high"}
              options={{
                kind: "status",
                statusMap: {
                  high: { tone: "danger", label: "High" },
                  low: { tone: "neutral", label: "Low" },
                },
              }}
            />
          }
        />
      ),
      type: "{ kind: 'status', statusMap: Record<string, { tone: Tone; label?: string }> }",
    },
    array: {
      description: (
        <FormatInlineExample
          value={["alpha", "beta", "gamma"]}
          output={
            <ArrayValue
              value={["alpha", "beta", "gamma"]}
              options={{ kind: "array", maxVisible: 2 }}
            />
          }
        />
      ),
      type: "{ kind: 'array', maxVisible?: number }",
    },
  }}
/>

## Accessibility

- Uses semantic `<table>` markup with proper header associations
- Sortable columns are keyboard-accessible via shadcn/ui Button
- Row selection uses standard checkbox patterns from Radix


## Links discovered
- [Image](https://github.com/assistant-ui/tool-ui/blob/main/docs/image.md)
- [Video](https://github.com/assistant-ui/tool-ui/blob/main/docs/video.md)
- [Link Preview](https://github.com/assistant-ui/tool-ui/blob/main/docs/link-preview.md)
- [Option List](https://github.com/assistant-ui/tool-ui/blob/main/docs/option-list.md)
- [`components/tool-ui/data-table`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table)
- [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [shadcn/ui](https://ui.shadcn.com)
- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Data Table (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table)
- [ZIP](https://download-directory.github.io/?url=https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/data-table)

--- app/docs/design-guidelines/content.mdx ---
import { DocsHeader } from "../_components/docs-header";
import { MockThread, MockMessage } from "../_components/mock-thread";
import { CollaborationDiagram } from "../_components/collaboration-diagram";
import { DataTable } from "@/components/tool-ui/data-table";
import { OptionList } from "@/components/tool-ui/option-list";

<DocsHeader
  title="Design Guidelines"
  description="Principles for agentic UI. A work in progress."
  mdxPath="app/docs/design-guidelines/content.mdx"
/>

## The Collaboration Model

In AI chat, users interact with both the assistant and any tool UIs it renders. Most tool UIs treat this relationship as incidental: a tool UI appears, the user clicks something, done. We treat it as foundational. The assistant, the tool UI, and the user form a collaborative triad.

<CollaborationDiagram />

The assistant contextualizes, interprets, and narrates. The tool UI provides structure that prose cannot: sortable tables, precise controls, rich media. Neither replaces the other.

<div className="mx-auto max-w-2xl">
  <MockThread caption="The triadic loop in action">
    <MockMessage role="user">
      <span className="text-sm">find a good auth library for React</span>
    </MockMessage>
    <MockMessage role="assistant">
      <span className="text-foreground">
        Found 8 React auth libraries on GitHub. Here are the top results by
        recent activity:
      </span>
      <div className="mt-3">
        <DataTable
          rowIdKey="id"
          columns={[
            {
              key: "name",
              label: "Repository",
              priority: "primary",
              format: { kind: "link", hrefKey: "url", external: true },
            },
            {
              key: "stars",
              label: "Stars",
              align: "right",
              format: { kind: "number" },
            },
            {
              key: "weeklyChange",
              label: "Weekly",
              align: "right",
              format: { kind: "delta", decimals: 0, showSign: true },
            },
            {
              key: "issues",
              label: "Issues",
              align: "right",
              format: { kind: "number" },
            },
            {
              key: "license",
              label: "License",
              format: { kind: "badge" },
            },
          ]}
          data={[
            {
              id: "1",
              name: "next-auth",
              url: "https://github.com/nextauthjs/next-auth",
              stars: 19420,
              weeklyChange: 342,
              issues: 86,
              license: "MIT",
            },
            {
              id: "2",
              name: "passport",
              url: "https://github.com/jaredhanson/passport",
              stars: 847,
              weeklyChange: -12,
              issues: 124,
              license: "MIT",
            },
            {
              id: "3",
              name: "auth0-react",
              url: "https://github.com/auth0/auth0-react",
              stars: 3210,
              weeklyChange: 89,
              issues: 23,
              license: "MIT",
            },
            {
              id: "4",
              name: "firebase-auth",
              url: "https://github.com/nicholasgriffintn/firebase-auth",
              stars: 562,
              weeklyChange: 8,
              issues: 45,
              license: "Apache-2.0",
            },
            {
              id: "5",
              name: "clerk-sdk",
              url: "https://github.com/clerk/javascript",
              stars: 1580,
              weeklyChange: 156,
              issues: 31,
              license: "MIT",
            },
          ]}
        />
      </div>
    </MockMessage>
    <MockMessage role="user">
      <span className="text-sm">
        Which one would you recommend for a Next.js app?
      </span>
    </MockMessage>
    <MockMessage role="assistant">
      <span className="text-foreground">
        I recommend <strong>next-auth</strong>. It's purpose-built for Next.js,
        has by far the most stars, and was updated most recently. Want me to
        show you setup instructions?
      </span>
    </MockMessage>
  </MockThread>
</div>

The assistant introduces the tool UI, the user scans it and asks a follow-up, the assistant answers by referencing specific rows. This is the triadic loop.

## Roles

Every Tool UI has a primary role:

- **Information:** Display data (tables, cards, charts). The focus is on comprehension rather than interaction.

- **Decision:** Capture consequential choices (approve/reject, send/cancel). These require clear options and receipts that prove what happened.

- **Control:** Adjust parameters without commitment (filters, sort orders, date ranges). Changes are reversible.

- **State:** Expose internal activity (progress indicators, status logs, loading states).

Some tool UIs combine roles. A data table with row actions is "information with control." When combining, the primary role should dominate.

## Constraints

Tool UIs live in chat: narrow widths, variable heights, mixed with prose.

- **Vertical:** Communicate purpose within the first 300px or so.
- **Horizontal:** Expect 400–600px. Prefer single-column layouts.
- **Touch:** Interactive elements need at least 44×44px tap area.
- **Choices:** Limit visible options to 5–7. The assistant can offer to show more.

## Addressability

If the assistant can't point at something later, you lose half the value of rendering it.

- **`id`**: Identifies this specific tool UI in the conversation ("the preview above", "that table").
- **`assetId` / row IDs / option IDs**: Identify real objects ("that link", "row 4", "the merge option").

Prefer stable IDs derived from your backend (database IDs, canonical URLs) over array indexes or UUIDs generated at render time. When something can be acted on, it should have an ID the assistant can cite.

## Receipts

When a user takes an action with consequences, the tool UI should transition to a receipt state that confirms what happened. This gives the user proof and gives the assistant something to reference later.

<div className="mx-auto max-w-2xl">
  <MockThread caption="A decision tool UI with receipt">
    <MockMessage role="assistant">
      <span className="text-foreground">
        How would you like to handle the duplicate contacts?
      </span>
      <div className="mt-3">
        <OptionList
          selectionMode="single"
          confirmed="merge"
          options={[
            {
              id: "merge",
              label: "Merge duplicates",
              description: "Combine into single contacts, keeping all data",
            },
            {
              id: "keep",
              label: "Keep all",
              description: "Leave duplicates as separate contacts",
            },
            {
              id: "review",
              label: "Review manually",
              description: "I'll decide for each duplicate",
            },
          ]}
        />
      </div>
    </MockMessage>
    <MockMessage role="assistant">
      <span className="text-foreground">
        Done. I merged 12 duplicate contacts. You can undo this from Settings →
        Contact History if needed.
      </span>
    </MockMessage>
  </MockThread>
</div>

The receipt shows what the user chose. The assistant's follow-up confirms the outcome and offers a way to reverse it.

## Anti-Patterns

- **Input fields:** Input fields introduce dissonance by competing with the primary composer input. Reflect on whether it'd be more appropriate to capture that information through dialogue with the asssitant or, in rare cases, link to a dedicated form outside of or detached from the the chat context.
- **Hidden mutations:** Actions that change state should produce visible indication of what happened; terminal decisions should produce a receipt that reflects what choice the user made.
- **Kitchen sinks:** If it needs tabs or navigation, consider breaking it into separate Tool UIs.
- **Uncontextualized tool UIs:** Always have the assistant introduce a tool UI, providing context that clarifies what's being shown.
- **Redundant narration:** The assistant shouldn't describe what the tool UI already shows. Conversely, the tool UI shouldn't echo what the assistant just said. If the assistant asks "Where to?", the tool UI doesn't need a header saying "Where to?"; just show the options. Divide the work: the assistant provides context, the tool UI provides structure.


--- app/docs/image/content.mdx ---
import { DocsHeader } from "../_components/docs-header";
import { Image } from "@/components/tool-ui/image";

<DocsHeader
  title="Image"
  description="Display images with metadata and attribution."
  mdxPath="app/docs/image/content.mdx"
/>

<Tabs items={["Preview", "Code"]}>
  <Tab>
    <div className="not-prose mx-auto max-w-md">
      <Image
          id="image-example"
          assetId="vintage-mainframe"
          src="https://images.unsplash.com/photo-1504548840739-580b10ae7715?w=1200&auto=format&fit=crop"
          alt="Vintage mainframe with blinking lights"
          title="From mainframes to microchips"
          description="A snapshot of when rooms were computers."
          ratio="4:3"
          domain="unsplash.com"
        />
    </div>
  </Tab>
  <Tab>
    ```tsx
    import { Image } from "@/components/tool-ui/image";

    export function Example() {
      return (
        <Image
          id="image-example"
          assetId="vintage-mainframe"
          src="https://images.unsplash.com/photo-1504548840739-580b10ae7715"
          alt="Vintage mainframe with blinking lights"
          title="From mainframes to microchips"
          description="A snapshot of when rooms were computers."
          ratio="4:3"
          domain="unsplash.com"
        />
      );
    }
    ```
  </Tab>
</Tabs>

## Key Features

<FeatureGrid>
  <Feature icon="Eye" title="Hover overlay">
    Title and source attribution appear on hover
  </Feature>
  <Feature icon="ExternalLink" title="Source attribution">
    Click-through to source with icon and domain
  </Feature>
  <Feature icon="Maximize2" title="Aspect ratios">
    Support for 1:1, 4:3, 16:9, 9:16, or auto
  </Feature>
</FeatureGrid>

## When to Use

- **Good for:** Image thumbnails, photo previews, visual content with attribution
- **Not for:** Image galleries (use [Item Carousel](/docs/item-carousel)), video content (use [Video](/docs/video))

## Source and Install

Copy [`components/tool-ui/image`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/image) and the [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared) directory into your project.

### Prerequisites

```bash
pnpm dlx shadcn@latest add badge button card tooltip
```

### Download

- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Image (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/image)

## Props

<TypeTable
  type={{
    id: {
      description: "Unique identifier for this tool UI instance",
      type: "string",
      required: true,
    },
    assetId: {
      description: "Persistent asset identifier",
      type: "string",
      required: true,
    },
    src: {
      description: "Image source URL",
      type: "string",
      required: true,
    },
    alt: {
      description: "Alt text for accessibility",
      type: "string",
      required: true,
    },
    title: { description: "Title text", type: "string" },
    description: { description: "Description", type: "string" },
    href: { description: "Makes image clickable with this URL", type: "string" },
    domain: { description: "Source domain label", type: "string" },
    ratio: {
      description: "Aspect ratio",
      type: "'auto' | '1:1' | '4:3' | '16:9' | '9:16'",
      default: "'auto'",
    },
    fit: {
      description: "Object fit mode",
      type: "'cover' | 'contain'",
      default: "'cover'",
    },
    source: {
      description: "Source attribution",
      type: "{ label: string; iconUrl?: string; url?: string }",
    },
    createdAt: { description: "Creation timestamp (ISO 8601)", type: "string" },
    fileSizeBytes: { description: "File size in bytes", type: "number" },
    responseActions: { description: "Response action buttons", type: "Action[]" },
    onResponseAction: { description: "Response action handler", type: "(actionId: string) => void" },
  }}
/>


## Links discovered
- [Item Carousel](https://github.com/assistant-ui/tool-ui/blob/main/docs/item-carousel.md)
- [Video](https://github.com/assistant-ui/tool-ui/blob/main/docs/video.md)
- [`components/tool-ui/image`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/image)
- [`shared`](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Shared Dependencies (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/shared)
- [**Image (GitHub)**](https://github.com/assistant-ui/tool-ui/tree/main/components/tool-ui/image)

--- app/api/chat/route.ts ---
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";
import { z } from "zod";
import { checkRateLimit } from "@/lib/integrations/rate-limit/upstash";
import { getStocks } from "@/lib/mocks/stocks";

export const runtime = "edge";

export async function POST(req: Request) {
  try {
    if (!process.env.OPENAI_API_KEY) {
      return new Response(
        JSON.stringify({
          error:
            "OpenAI API key is not configured. Please set OPENAI_API_KEY in your environment variables.",
        }),
        {
          status: 500,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    const ip =
      req.headers.get("x-forwarded-for")?.split(",")[0] ||
      req.headers.get("x-real-ip") ||
      "anonymous";

    const rateLimitResult = await checkRateLimit(ip);

    if (!rateLimitResult.success) {
      return new Response(
        JSON.stringify({
          error: "Rate limit exceeded. Please try again later.",
          reset: rateLimitResult.reset,
        }),
        {
          status: 429,
          headers: {
            "Content-Type": "application/json",
            "X-RateLimit-Limit": rateLimitResult.limit.toString(),
            "X-RateLimit-Remaining": rateLimitResult.remaining.toString(),
            "X-RateLimit-Reset": rateLimitResult.reset.toString(),
          },
        },
      );
    }

    const { messages } = await req.json();

    if (!messages || !Array.isArray(messages)) {
      return new Response(
        JSON.stringify({ error: "Invalid request: messages array required" }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    const modelMessages = convertToModelMessages(messages);

    const result = streamText({
      model: openai("gpt-5-nano"),
      messages: modelMessages,
      system: `You are a helpful assistant that can provide stock market data and visualize it using tables.

When asked about stocks or market data, use the get_stocks tool to retrieve information. The tool will display data in a responsive table that works on both desktop and mobile devices.

Be concise and helpful in your responses.`,
      tools: {
        get_stocks: {
          description:
            "Get stock market data for one or more companies. Returns current prices, changes, volume, and market cap information. Can filter by specific stock symbols and sort results.",
          inputSchema: z.object({
            query: z
              .string()
              .describe(
                "Natural language query about stocks. Examples: 'top tech stocks', 'AAPL price', 'MSFT GOOGL AMZN data'",
              ),
          }),
          execute: async ({ query }) => {
            const upperQuery = query.toUpperCase();
            const commonSymbols = [
              "AAPL",
              "MSFT",
              "GOOGL",
              "AMZN",
              "TSLA",
              "NVDA",
              "META",
              "BRK.B",
            ];
            const mentionedSymbols = commonSymbols.filter((symbol) =>
              upperQuery.includes(symbol),
            );

            const isTechQuery =
              upperQuery.includes("TECH") ||
              upperQuery.includes("TECHNOLOGY") ||
              upperQuery.includes("SOFTWARE");

            const stocks = await getStocks({
              symbols:
                mentionedSymbols.length > 0 ? mentionedSymbols : undefined,
              limit: isTechQuery ? 6 : undefined,
              sort: { by: "marketCap", direction: "desc" },
            });

            return {
              stocks,
              count: stocks.length,
            };
          },
        },
      },
      temperature: 0.7,
    });

    return result.toUIMessageStreamResponse();
  } catch (error) {
    console.error("Error in chat API route:", error);
    return new Response(
      JSON.stringify({
        error: "An error occurred while processing your request.",
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      },
    );
  }
}


--- app/api/mcp-tools/route.ts ---
import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";

export const runtime = "edge";

interface MCPTool {
  name: string;
  description?: string;
  inputSchema: {
    type: string;
    properties?: Record<string, unknown>;
    required?: string[];
  };
}

export async function POST(req: Request) {
  try {
    const { serverUrl, transportType = "http" } = await req.json();

    if (!serverUrl || typeof serverUrl !== "string") {
      return new Response(
        JSON.stringify({ error: "Invalid request: serverUrl is required" }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    // Validate URL format
    try {
      new URL(serverUrl);
    } catch {
      return new Response(
        JSON.stringify({
          error: "Invalid URL format",
        }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    // Use Vercel AI SDK's MCP client
    console.log(
      `[MCP] Creating MCP client for ${serverUrl} with transport: ${transportType}`,
    );

    try {
      const mcpClient = await createMCPClient({
        transport: {
          type: transportType as "http" | "sse",
          url: serverUrl,
        },
      });

      console.log(`[MCP] Client created, fetching tools...`);
      const tools = await mcpClient.tools();
      console.log(`[MCP] Received ${Object.keys(tools).length} tools`);

      // Convert AI SDK tool format to our format
      const mcpTools: MCPTool[] = Object.entries(tools).map(([name, tool]) => ({
        name,
        description: tool.description,
        inputSchema: tool.inputSchema as unknown as {
          type: string;
          properties?: Record<string, unknown>;
          required?: string[];
        },
      }));

      // Close the client
      await mcpClient.close();

      return new Response(
        JSON.stringify({
          tools: mcpTools,
          serverUrl,
          count: mcpTools.length,
        }),
        {
          status: 200,
          headers: { "Content-Type": "application/json" },
        },
      );
    } catch (error) {
      console.error(`[MCP] Error with AI SDK client:`, error);

      return new Response(
        JSON.stringify({
          error: `Could not connect to MCP server: ${error instanceof Error ? error.message : "Unknown error"}`,
        }),
        {
          status: 500,
          headers: { "Content-Type": "application/json" },
        },
      );
    }
  } catch (error) {
    console.error("Error fetching MCP tools:", error);
    return new Response(
      JSON.stringify({
        error: "An error occurred while fetching MCP tools",
        details: error instanceof Error ? error.message : "Unknown error",
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      },
    );
  }
}


--- app/api/builder/chat/route.ts ---
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, convertToModelMessages, stepCountIs } from "ai";
import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { checkRateLimit } from "@/lib/integrations/rate-limit/upstash";
import { requestDevServer } from "@/lib/integrations/freestyle/create-chat";
import { SYSTEM_MESSAGE } from "@/lib/system/tool-builder-message";

export const maxDuration = 300;

export async function POST(req: Request) {
  try {
    if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
      return new Response(
        JSON.stringify({
          error:
            "API key is not configured. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment variables.",
        }),
        {
          status: 500,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    const ip =
      req.headers.get("x-forwarded-for")?.split(",")[0] ||
      req.headers.get("x-real-ip") ||
      "anonymous";

    const rateLimitResult = await checkRateLimit(ip);

    if (!rateLimitResult.success) {
      return new Response(
        JSON.stringify({
          error: "Rate limit exceeded. Please try again later.",
          reset: rateLimitResult.reset,
        }),
        {
          status: 429,
          headers: {
            "Content-Type": "application/json",
            "X-RateLimit-Limit": rateLimitResult.limit.toString(),
            "X-RateLimit-Remaining": rateLimitResult.remaining.toString(),
            "X-RateLimit-Reset": rateLimitResult.reset.toString(),
          },
        },
      );
    }

    const { messages } = await req.json();
    const repoId = req.headers.get("Repo-Id");

    if (!messages || !Array.isArray(messages)) {
      return new Response(
        JSON.stringify({ error: "Invalid request: messages array required" }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    const modelMessages = convertToModelMessages(messages);

    // Choose model based on what's available
    const model = process.env.ANTHROPIC_API_KEY
      ? anthropic("claude-sonnet-4-5-20250929")
      : openai("gpt-5-nano");

    // If we have a repoId and Freestyle is configured, use Freestyle tools
    let tools = {};
    if (repoId && process.env.FREESTYLE_API_KEY) {
      try {
        const { mcpEphemeralUrl } = await requestDevServer({ repoId });

        const devServerMcp = await createMCPClient({
          transport: new StreamableHTTPClientTransport(
            new URL(mcpEphemeralUrl),
          ),
        });

        tools = await devServerMcp.tools();
      } catch (error) {
        console.error("Error setting up Freestyle MCP:", error);
        // Continue without Freestyle tools if there's an error
      }
    }

    const result = streamText({
      model,
      messages: modelMessages,
      system: SYSTEM_MESSAGE,
      tools,
      stopWhen: stepCountIs(100),
      temperature: 0.7,
    });

    result.consumeStream();

    return result.toUIMessageStreamResponse();
  } catch (error) {
    console.error("Error in builder chat API route:", error);
    return new Response(
      JSON.stringify({
        error: "An error occurred while processing your request.",
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      },
    );
  }
}


--- app/api/builder/create-freestyle/route.ts ---
import { createChat } from "@/lib/integrations/freestyle/create-chat";

export async function POST() {
  try {
    if (!process.env.FREESTYLE_API_KEY) {
      return new Response(
        JSON.stringify({
          error:
            "Freestyle API key is not configured. Please set FREESTYLE_API_KEY in your environment variables.",
        }),
        {
          status: 500,
          headers: { "Content-Type": "application/json" },
        },
      );
    }

    const { repoId, ephemeralUrl, mcpEphemeralUrl } = await createChat();

    return new Response(
      JSON.stringify({
        repoId,
        ephemeralUrl,
        mcpEphemeralUrl,
      }),
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      },
    );
  } catch (error) {
    console.error("Error creating Freestyle project:", error);
    return new Response(
      JSON.stringify({
        error: "Failed to create Freestyle project.",
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      },
    );
  }
}


--- app/api/playground/chat/route.ts ---
import type { NextRequest } from "next/server";

import type { UIMessage } from "ai";

import { findPrototype, streamPrototypeResponse } from "@/lib/playground";
import { PROTOTYPE_SLUG_HEADER } from "@/lib/playground/constants";

const isUiMessageArray = (value: unknown): value is UIMessage[] =>
  Array.isArray(value);

const extractSlug = (request: NextRequest): string | null => {
  const headerSlug = request.headers.get(PROTOTYPE_SLUG_HEADER)?.trim();
  if (headerSlug) {
    return headerSlug;
  }
  const url = new URL(request.url);
  const querySlug = url.searchParams.get("slug")?.trim();
  if (querySlug) {
    return querySlug;
  }
  return null;
};

export async function POST(request: NextRequest) {
  const slug = extractSlug(request);
  if (!slug) {
    return new Response(
      JSON.stringify({ error: "Missing prototype slug." }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" },
      },
    );
  }

  const prototype = findPrototype(slug);
  if (!prototype) {
    return new Response(
      JSON.stringify({ error: `Prototype "${slug}" not found.` }),
      {
        status: 404,
        headers: { "Content-Type": "application/json" },
      },
    );
  }

  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return new Response(
      JSON.stringify({ error: "Invalid JSON body." }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" },
      },
    );
  }

  const messages =
    typeof body === "object" && body !== null && "messages" in body
      ? (body as Record<string, unknown>).messages
      : undefined;

  if (!isUiMessageArray(messages)) {
    return new Response(
      JSON.stringify({ error: "Request body must include a messages array." }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" },
      },
    );
  }

  if (process.env.NODE_ENV !== "production") {
    try {
      const lastAssistantWithTools = [...messages]
        .reverse()
        .find(
          (message) =>
            message.role === "assistant" &&
            Array.isArray(message.parts) &&
            message.parts.some(
              (part) => typeof part?.type === "string" && part.type.startsWith("tool-"),
            ),
        );
      if (lastAssistantWithTools) {
        const toolParts = lastAssistantWithTools.parts?.filter(
          (part) => typeof part?.type === "string" && part.type.startsWith("tool-"),
        );
        console.debug(
          "[playground] forwarding tool parts:",
          JSON.stringify(toolParts, null, 2),
        );
      }
    } catch (error) {
      console.warn("[playground] failed to log tool parts", error);
    }
  }

  // Extract frontend tools from request body
  const clientTools: unknown =
    typeof body === "object" && body !== null && "tools" in body
      ? (body as Record<string, unknown>).tools
      : undefined;

  const result = streamPrototypeResponse(prototype, messages, clientTools);
  return result.toUIMessageStreamResponse();
}


--- lib/playground/prototypes/waymo-v2/DESIGN.md ---
# Waymo v2 Prototype Design

## Purpose

This prototype exists to **research and demonstrate Tool UI patterns** using ride booking as a concrete domain. The goal isn't a production Waymo clone - it's to use this vertical as a lens for understanding:

1. When Tool UIs add value vs. plain text
2. What fundamental patterns exist across domains
3. How Tool UIs compose in conversations
4. What data contracts work between LLMs and UIs

## Relationship to Design Guidelines

This prototype and the [Design Guidelines](/docs/design-guidelines) evolve together:

- **Guidelines → Prototype**: We follow the principles (triadic loop, receipts, anti-patterns)
- **Prototype → Guidelines**: Friction we hit here informs new guidelines

### Learnings Fed Back to Guidelines

| Discovery | Guideline Update |
|-----------|------------------|
| Assistant said "Where to?" and UI had "Where to?" header | Expanded "Redundant narration" to be bidirectional — neither assistant nor surface should echo the other |
| Pickup location change needed dedicated UI, not inline edit | Secondary actions that require selection should spawn new Tool UIs rather than inline editing |
| Changing a term of a "contract" UI (like RideQuote) invalidates it | New pattern: **Contract with Revocation**. Some Tool UIs represent a contract (offer with specific terms). Changing any term revokes the contract and requires a fresh one. The revoked UI shows "superseded" state, a new Tool UI appears with updated terms. |
| User can type "take me home" instead of clicking the picker | Design decision: **Skip redundant selection UIs**. When destination is implicit in user's message, skip the picker and go straight to the quote. The quote shows the destination anyway, so a separate confirmation is redundant. Selection UIs are for clarification, not confirmation. |

---

## The Triadic Loop

From the [Design Guidelines](/docs/design-guidelines): The assistant, the surface, and the user form a **collaborative triad**.

```
    User
   ↙    ↘
controls  observes
   ↓        ↓
 Surface ←→ Assistant
  mediates   narrates
```

**The loop in action:**

1. **Assistant introduces** the surface with context
2. **User interacts** with the surface
3. **Surface mediates** the result back
4. **Assistant narrates** what happened and continues

This is NOT just "show UI, wait for click." The assistant must:
- **Introduce** each surface ("Where would you like to go?")
- **Acknowledge** user actions ("Great, Home it is.")
- **Reference** surface data in follow-ups ("Your 5-minute ride to Home...")
- **Contextualize** what comes next ("Let me get you a quote.")

### Anti-pattern: Silent Surfaces

❌ **Wrong:**
```
User: "I need a ride"
[DestinationPicker appears silently]
User: clicks Home
[RideQuote appears silently]
```

✅ **Right:**
```
User: "I need a ride"
Assistant: "Where would you like to go?"
[DestinationPicker appears]
User: clicks Home
Assistant: "Home it is! Here's your quote for the 5-minute ride."
[RideQuote appears]
```

### Implementation: How the Loop Flows

With `makeAssistantTool` and `type: "human"`:

1. **Assistant calls tool** → Tool UI renders in interactive state
2. **User interacts** → Component calls `addResult({ ...data, selectedLocation })`
3. **Result sent to model** → Assistant sees the selection in the tool result
4. **Assistant generates response** → Can reference the selection and call next tool

The model sees the full result object, so it knows:
- What the user selected (`selectedLocation.label === "Home"`)
- Any metadata (ETA, price from quote, etc.)

This enables natural acknowledgment: "Home it is! That's a 5-minute ride for $12.50."

---

## Core User Experience

### User Mental Model

When someone says "I need a ride", they're thinking:

- **Where am I?** (usually known/assumed)
- **Where am I going?** (sometimes known, sometimes vague)
- **How much / how long?** (want to know before committing)
- **Is this confirmed?** (need clear feedback)

### UX Principles

1. **Minimize required input** - Use context (GPS, saved locations) to reduce questions
2. **Show, don't ask** - Present options visually rather than asking open-ended questions
3. **Single clear action** - Each Tool UI should have one obvious next step
4. **Recoverable** - User can always correct/change their mind
5. **Progressive disclosure** - Don't overwhelm with details upfront

---

## Conversation Flow

### Entry Points

| Entry | Example | Handling |
|-------|---------|----------|
| No destination | "I need a ride" | Show DestinationPicker |
| Known intent | "Take me home" | Resolve from profile, show RideQuote |
| Price check | "How much to SFO?" | Show RideQuote (no immediate booking) |
| Full intent | "Book a Waymo to 123 Main St" | Resolve address, show RideQuote |

### The Golden Path

```
User: "I need a ride"
    │
    ▼
┌──────────────────────────────────────┐
│  TOOL UI: DestinationPicker          │  ← Pattern: SELECTION
│  ─────────────────────────────────── │
│  "Where to?"                         │
│                                      │
│  [🏠 Home]  [💼 Work]                │
│  [📍 Recent: Ferry Building]         │
│                                      │
│  ─────────────────────────────────── │
│  User clicks "Home"                  │
│  → UI transforms to receipt:         │
│  "✓ Home - 123 Main St"              │
└──────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────┐
│  TOOL UI: RideQuote                  │  ← Pattern: CONFIRMATION
│  ─────────────────────────────────── │
│  Downtown Coffee → Home              │
│  5 min • $12.50 • Waymo One          │
│  Payment: Apple Pay                  │
│                                      │
│  [ Confirm Ride ]                    │
│                                      │
│  ─────────────────────────────────── │
│  User clicks "Confirm"               │
│  → UI transforms to receipt:         │
│  "✓ Ride confirmed • 5 min away"     │
└──────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────┐
│  TOOL UI: TripStatus                 │  ← Pattern: PROGRESS
│  ─────────────────────────────────── │
│  ✓ Requested                         │
│  ● Vehicle on the way (3 min)        │
│  ○ Pickup                            │
│  ○ Dropoff                           │
│                                      │
│  White Jaguar I-PACE • 8ABC123       │
│                                      │
│  [Cancel Ride]                       │
│  ─────────────────────────────────── │
│  (Updates live as status changes)    │
│  → Eventually becomes receipt:       │
│  "✓ Trip completed • $12.50"         │
└──────────────────────────────────────┘
```

---

## Tool UI Patterns

This prototype exercises three fundamental patterns that generalize across domains:

### 1. Selection Pattern (DestinationPicker)

**Purpose:** User chooses from discrete options

**Behavior:**
- Displays options visually
- User clicks to select
- Transforms to receipt showing choice

**Generalizes to:** Payment methods, ride types, menu items, time slots, any multi-choice input

**Library needs:**
- `addResult()` for capturing user choice
- Receipt state detection from result data
- Option rendering primitives

#### When to Skip Selection UIs

Selection tools are for **clarification**, not confirmation. If the user's intent is already clear from their message, skip the selection UI and proceed to the next step.

```
User: "I need a ride"           → Show DestinationPicker (need clarification)
User: "Take me home"            → Skip picker, go to RideQuote (destination clear)
User: "How much to the Ferry?"  → Skip picker, go to RideQuote (destination clear)
```

The key insight: Don't show a UI that asks a question the user already answered. If the downstream UI (RideQuote) displays the destination anyway, a separate selection receipt is redundant.

---

### 2. Confirmation / Contract Pattern (RideQuote)

**Purpose:** Present a contract (offer with specific terms) for user approval

**Behavior:**
- Shows key details in scannable format
- Primary action: Accept (Confirm)
- Secondary actions: Modify terms (e.g., "Change pickup location")
- **If terms change → contract is revoked**, UI transitions to "superseded" state
- New contract must be presented with updated terms
- Transforms to receipt on acceptance

**States:**
- `interactive` → User reviewing the contract
- `revoked` → User changed a term, contract superseded (dimmed, shows "Quote updated")
- `confirmed` → User accepted, contract fulfilled (receipt)

**Generalizes to:** Order summaries, booking confirmations, transaction approvals, pricing quotes, any offer that can be modified before acceptance

**Key insight:** A contract is a snapshot of specific terms. You can't "edit" a contract in place—you revoke and replace it. This maps naturally to conversation flow where each Tool UI is a discrete event.

**Library needs:**
- Structured card layouts
- Primary + secondary action buttons
- Revoked/superseded state styling
- Receipt state for accepted contracts

---

### 3. Progress Pattern (TripStatus)

**Purpose:** Show live-updating state over time

**Behavior:**
- Timeline/stepper visualization
- Updates as external state changes
- Actions available at certain states (e.g., Cancel before pickup)
- Eventually reaches terminal state → receipt

**Generalizes to:** Order tracking, file uploads, deployment status, any long-running task

**Library needs:**
- Polling/subscription mechanism for updates
- State machine visualization components
- Conditional action availability
- Terminal state detection

---

## Tool UI Components

| Component | Pattern | States |
|-----------|---------|--------|
| **DestinationPicker** | Selection | interactive → receipt |
| **PickupPicker** | Selection | interactive → receipt |
| **RideQuote** | Contract | interactive → revoked \| confirmed |
| **TripStatus** | Progress | live (multiple phases) → completed \| cancelled |

---

## Research Questions

### When does a Tool UI add value?

- Visual location picker vs. "type your destination"
- Showing a card vs. just saying "Your ride is $12.50"
- Hypothesis: Tool UIs win when there's **structured data** or **discrete choices**

### How do Tool UIs compose?

- One tool call → one UI, or chained?
- How do receipts work with multiple UIs in thread?
- What happens when a UI needs to update after user action?

### What's the right data contract?

- Should LLM pass presentation-ready strings or raw data?
- How much should UI "know" vs. just render props?
- Where does formatting/localization happen?

---

## Open Design Questions

1. **Zero-input vs. confirmation**
   - If we know home + GPS, can "ride home" skip DestinationPicker?
   - Or should we always show a confirmation step?

2. **LLM value-add**
   - LLM excels at ambiguous intent ("somewhere for lunch nearby")
   - Tool UIs excel at structured selection
   - Where are the handoff points?

3. **Mid-flow corrections**
   - User confirms, then says "wait, wrong address"
   - Cancel and restart? Inline edit?

---

## Next Steps

1. Define component interfaces for the three Tool UIs
2. Build minimal implementations that exercise the patterns
3. Identify library primitives needed to support these patterns
4. Document learnings for Tool UI design guidelines

---

*Last updated: 2025-01-25*

## Links discovered
- [Design Guidelines](https://github.com/assistant-ui/tool-ui/blob/main/docs/design-guidelines.md)

--- LICENSE.md ---
MIT License

Copyright (c) 2025 AgentbaseAI Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


--- README.md ---
# Tool UI

### Beautiful UI components for AI tool calls.

Responsive, accessible, typed, copy-pasteable. Built on Radix, shadcn/ui, and Tailwind. Open Source.

## Components

- Chart — Visualize data with interactive charts
- Code Block — Display syntax-highlighted code snippets
- Data Table — Sortable columns, row actions, loading/empty states, mobile accordion layout
- Media Card — Image/video/audio/link cards; OG previews; alt-text validation
- Option List — Single/multi-select choices with configurable response actions
- Plan — Display step-by-step task workflows
- Social Post — X/Instagram/LinkedIn renderers; media + link previews
- Terminal — Show command-line output and logs

👀 [Browse components](https://tool-ui.com/components)

## Builder

- Prompt -> Tool UI
- Connect an MCP server (HTTP or SSE) and auto-discover tools
- Auto-generate UI from a tool’s JSON schema
- Toggle between rendered preview and code; just copy/paste the generated component

🏗️ [Builder](https://tool-ui.com/builder)

## Contributing

Contributions are welcome!

## License

MIT License — see the [LICENSE](LICENSE) file for details.


## Links discovered
- [Browse components](https://tool-ui.com/components)
- [Builder](https://tool-ui.com/builder)
- [LICENSE](https://github.com/assistant-ui/tool-ui/blob/main/LICENSE.md)

--- eslint.config.ts ---
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
  // Global ignores first
  globalIgnores([
    "**/dist/**",
    "**/node_modules/**",
    "**/.next/**",
    "**/out/**",
    "**/next-env.d.ts",
  ]),

  // Next.js recommended configs (native flat format in v16, includes React)
  ...nextVitals,
  ...nextTs,

  // Custom rules override
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        {
          ignoreRestSiblings: true,
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
        },
      ],
      // Disable strict React Compiler rules that don't fit this codebase's patterns
      "react-hooks/refs": "off",
      "react-hooks/immutability": "off",
      "react-hooks/set-state-in-effect": "off",
      "react-hooks/static-components": "off",
    },
  },
]);

export default eslintConfig;


--- next.config.ts ---
import { NextConfig } from "next";
import createMDX from "@next/mdx";

const config: NextConfig = {
  reactStrictMode: true,
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
  async redirects() {
    return [
      {
        source: "/components",
        destination: "/docs/gallery",
        permanent: true,
      },
      {
        source: "/components/:path*",
        destination: "/docs/:path*",
        permanent: true,
      },
    ];
  },
};

const withMDX = createMDX({});

export default withMDX(config);


--- vitest.config.ts ---
import { defineConfig } from "vitest/config";
import path from "path";

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./"),
    },
  },
  test: {
    environment: "node",
    include: ["lib/playground/**/*.test.ts", "app/workbench/lib/**/*.test.ts"],
    passWithNoTests: true,
    globals: true,
    coverage: {
      reporter: ["text", "json-summary"],
    },
  },
});


--- app/workbench/lib/README.md ---
# Workbench

A development environment for building and testing ChatGPT widget UIs. The workbench simulates the OpenAI Apps SDK `window.openai` API, allowing you to develop widgets locally before deploying to ChatGPT.

## Quick Start

```tsx
import { OpenAIProvider } from "@/app/workbench/lib/openai-context";
import { WorkbenchShell } from "@/app/workbench/components";

function App() {
  return (
    <OpenAIProvider>
      <WorkbenchShell />
    </OpenAIProvider>
  );
}
```

## SDK Simulation

The workbench provides a complete simulation of the OpenAI Apps SDK. All API calls are logged to the console panel for debugging.

### Globals

Access these via hooks from `@/app/workbench/lib/openai-context`:

| Global        | Hook                           | Type                                | Description                     |
| ------------- | ------------------------------ | ----------------------------------- | ------------------------------- |
| `theme`       | `useTheme()`                   | `"light" \| "dark"`                 | Current color scheme            |
| `locale`      | `useLocale()`                  | `string`                            | User's locale (e.g., "en-US")   |
| `displayMode` | `useDisplayMode()`             | `"inline" \| "pip" \| "fullscreen"` | Widget display mode             |
| `maxHeight`   | `useOpenAiGlobal("maxHeight")` | `number`                            | Max height in inline mode (px)  |
| `toolInput`   | `useToolInput()`               | `object`                            | Input props passed to widget    |
| `toolOutput`  | `useToolOutput()`              | `object \| null`                    | Current tool output             |
| `widgetState` | `useWidgetState()`             | `object`                            | Persistent widget state         |
| `safeArea`    | `useOpenAiGlobal("safeArea")`  | `SafeAreaInsets`                    | Safe area insets for fullscreen |
| `view`        | `useView()`                    | `View \| null`                      | Current view (modal/inline)     |

### Methods

| Method                            | Hook                       | Description                       |
| --------------------------------- | -------------------------- | --------------------------------- |
| `callTool(name, args)`            | `useCallTool()`            | Invoke an MCP tool                |
| `setWidgetState(state)`           | `useWidgetState()[1]`      | Update persistent state           |
| `requestDisplayMode({ mode })`    | `useRequestDisplayMode()`  | Request display mode change       |
| `sendFollowUpMessage({ prompt })` | `useSendFollowUpMessage()` | Send follow-up to ChatGPT         |
| `openExternal({ href })`          | `useOpenExternal()`        | Open URL in new tab               |
| `requestModal({ title, params })` | `useOpenAI().requestModal` | Show modal overlay                |
| `uploadFile(file)`                | `useUploadFile()`          | Upload file, returns `{ fileId }` |
| `getFileDownloadUrl({ fileId })`  | `useGetFileDownloadUrl()`  | Get download URL for file         |

### Example: Using SDK Methods

```tsx
import {
  useCallTool,
  useWidgetState,
  useRequestDisplayMode,
  useSendFollowUpMessage,
  useOpenExternal,
} from "@/app/workbench/lib/openai-context";

function MyWidget() {
  const callTool = useCallTool();
  const [state, setState] = useWidgetState({ count: 0 });
  const requestDisplayMode = useRequestDisplayMode();
  const sendFollowUpMessage = useSendFollowUpMessage();
  const openExternal = useOpenExternal();

  const handleAction = async () => {
    // Call an MCP tool
    const result = await callTool("my_tool", { param: "value" });

    // Update widget state (persists across renders)
    setState({ count: state.count + 1 });

    // Request fullscreen mode
    await requestDisplayMode({ mode: "fullscreen" });

    // Send a follow-up message to ChatGPT
    await sendFollowUpMessage({ prompt: "Tell me more about this" });

    // Open external link
    openExternal({ href: "https://example.com" });
  };

  return <button onClick={handleAction}>Do Action</button>;
}
```

## Keyboard Shortcuts

| Shortcut             | Action                  |
| -------------------- | ----------------------- |
| `⌘/Ctrl + Shift + D` | Toggle dark/light theme |
| `⌘/Ctrl + Shift + F` | Toggle fullscreen mode  |
| `⌘/Ctrl + K`         | Clear console           |

## Config Panel

The right sidebar provides controls for:

- **Device**: Desktop, Tablet, Mobile viewport simulation
- **Mode**: Inline, PiP, Fullscreen display modes
- **Max Height**: Inline mode height constraint (100-2000px)
- **Safe Area**: Fullscreen safe area insets
- **Theme**: Light/Dark mode toggle
- **Locale**: Language/region selection
- **View**: Current view state (modal/inline)

## Creating a Widget

Widgets are React components that use SDK hooks. See `components/tool-ui/poi-map/` for a complete example.

### File Structure

```
components/tool-ui/my-widget/
  index.tsx          # Barrel exports
  my-widget.tsx      # Main component (SDK-agnostic)
  schema.ts          # Zod schema for props
  _adapter.tsx       # UI primitive re-exports

app/workbench/lib/wrappers/
  my-widget-sdk.tsx  # SDK wrapper component
```

### SDK Wrapper Pattern

The SDK wrapper bridges the SDK context to your presentational component:

```tsx
// app/workbench/lib/wrappers/my-widget-sdk.tsx
import { MyWidget } from "@/components/tool-ui/my-widget";
import {
  useWidgetState,
  useDisplayMode,
  useTheme,
  useCallTool,
} from "../openai-context";

export function MyWidgetSDK(props: Record<string, unknown>) {
  const [widgetState, setWidgetState] = useWidgetState(DEFAULT_STATE);
  const displayMode = useDisplayMode();
  const theme = useTheme();
  const callTool = useCallTool();

  const handleAction = async () => {
    await callTool("my_action", { data: widgetState });
  };

  return (
    <MyWidget
      {...props}
      displayMode={displayMode}
      theme={theme}
      widgetState={widgetState}
      onStateChange={setWidgetState}
      onAction={handleAction}
    />
  );
}
```

## Architecture

```
app/workbench/lib/
  openai-context.tsx   # SDK simulation via React Context
  store.ts             # Zustand store for workbench state
  types.ts             # TypeScript types
  file-store.ts        # In-memory file storage simulation
  component-registry.tsx # Widget registry
  wrappers/            # SDK wrapper components

app/workbench/components/
  workbench-shell.tsx  # Main shell layout
  config-panel.tsx     # Settings sidebar
  unified-workspace.tsx # Widget preview area
  inspector-panel.tsx  # Console/state inspector
  event-console.tsx    # SDK call log
```

## Console Panel

The bottom panel shows:

- **All**: All SDK interactions
- **Tool Calls**: `callTool` invocations and responses
- **State**: `setWidgetState` updates
- **Display**: `requestDisplayMode` transitions
- **Other**: `openExternal`, `sendFollowUpMessage`, etc.

Each entry shows:

- Method name and arguments
- Timestamp
- Expandable JSON for complex data
- Copy button for entries


--- app/workbench/lib/file-store.ts ---
interface StoredFile {
  file: File;
  objectUrl: string;
}

const fileStore = new Map<string, StoredFile>();

export function storeFile(file: File): string {
  const fileId = crypto.randomUUID();
  const objectUrl = URL.createObjectURL(file);
  fileStore.set(fileId, { file, objectUrl });
  return fileId;
}

export function getFileUrl(fileId: string): string | null {
  return fileStore.get(fileId)?.objectUrl ?? null;
}

export function clearFiles(): void {
  for (const { objectUrl } of fileStore.values()) {
    URL.revokeObjectURL(objectUrl);
  }
  fileStore.clear();
}

export function getFileCount(): number {
  return fileStore.size;
}


--- app/workbench/components/index.ts ---
export { WorkbenchShell } from "./workbench-shell";
export { UnifiedWorkspace } from "./unified-workspace";
export { ConfigPanel } from "./config-panel";
export { EventConsole } from "./event-console";
export { JsonEditor } from "./json-editor";
export { InspectorPanel } from "./inspector-panel";
export { ModalOverlay } from "./modal-overlay";


--- app/workbench/lib/index.ts ---
export * from "./types";

export { useWorkbenchStore } from "./store";
export {
  useSelectedComponent,
  useDisplayMode as useWorkbenchDisplayMode,
  useWorkbenchTheme,
  useDeviceType,
  useConsoleLogs,
  useToolInput as useWorkbenchToolInput,
  useToolOutput as useWorkbenchToolOutput,
} from "./store";

export { generateBridgeScript, generateComponentBundle } from "./openai-bridge";

export {
  workbenchComponents,
  getComponent,
  getComponentIds,
  type WorkbenchComponentEntry,
  type ComponentCategory,
} from "./component-registry";

export {
  OpenAIProvider,
  useOpenAI,
  useOpenAiGlobal,
  useToolInput,
  useToolOutput,
  useTheme,
  useDisplayMode,
  useLocale,
  useWidgetState,
  useCallTool,
  useRequestDisplayMode,
  useSendFollowUpMessage,
} from "./openai-context";

export {
  handleMockToolCall,
  registerMockHandler,
  getAvailableMockTools,
} from "./mock-responses";

export { OptionListSDK } from "./wrappers";


--- app/workbench/lib/mock-responses.ts ---
import type { CallToolResponse } from "./types";
import type { MockConfigState, MockResponse } from "./mock-config";

type MockHandler = (args: Record<string, unknown>) => Promise<CallToolResponse>;

export interface MockToolCallResult extends CallToolResponse {
  _mockVariant?: string;
}

const mockHandlers: Record<string, MockHandler> = {
  search: async (args) => {
    await simulateDelay(500);
    return {
      structuredContent: {
        results: [
          { id: "1", title: `Result for "${args.query || "search"}"`, score: 0.95 },
          { id: "2", title: "Another result", score: 0.87 },
          { id: "3", title: "Third result", score: 0.72 },
        ],
        total: 3,
      },
    };
  },

  get_weather: async (args) => {
    await simulateDelay(300);
    const location = (args.location as string) || "San Francisco";
    return {
      structuredContent: {
        location,
        temperature: 72,
        condition: "sunny",
        humidity: 45,
        forecast: [
          { day: "Today", high: 75, low: 58 },
          { day: "Tomorrow", high: 73, low: 56 },
        ],
      },
    };
  },

  create_item: async (args) => {
    await simulateDelay(400);
    return {
      structuredContent: {
        success: true,
        id: crypto.randomUUID(),
        created_at: new Date().toISOString(),
        ...args,
      },
    };
  },

  delete_item: async (args) => {
    await simulateDelay(200);
    return {
      structuredContent: {
        success: true,
        deleted_id: args.id,
      },
    };
  },

  list_items: async () => {
    await simulateDelay(350);
    return {
      structuredContent: {
        items: [
          { id: "1", name: "Item 1", status: "active" },
          { id: "2", name: "Item 2", status: "pending" },
          { id: "3", name: "Item 3", status: "completed" },
        ],
        total: 3,
        page: 1,
      },
    };
  },

  refresh: async () => {
    await simulateDelay(600);
    return {
      structuredContent: {
        refreshed: true,
        timestamp: new Date().toISOString(),
      },
    };
  },

  refresh_pois: async (args) => {
    await simulateDelay(800);
    return {
      structuredContent: {
        pois: [
          {
            id: "1",
            name: "Golden Gate Bridge",
            category: "landmark",
            lat: 37.8199,
            lng: -122.4783,
            description: "Iconic suspension bridge spanning the Golden Gate strait",
            rating: 4.8,
            imageUrl: "https://images.unsplash.com/photo-1449034446853-66c86144b0ad?w=400",
          },
          {
            id: "2",
            name: "Fisherman's Wharf",
            category: "entertainment",
            lat: 37.808,
            lng: -122.4177,
            description: "Historic waterfront with restaurants and attractions",
            rating: 4.3,
          },
          {
            id: "3",
            name: "Alcatraz Island",
            category: "museum",
            lat: 37.8267,
            lng: -122.4233,
            description: "Former federal prison, now a museum",
            rating: 4.7,
          },
          {
            id: "4",
            name: "Chinatown",
            category: "shopping",
            lat: 37.7941,
            lng: -122.4078,
            description: "Oldest Chinatown in North America",
            rating: 4.4,
          },
          {
            id: "5",
            name: "Golden Gate Park",
            category: "park",
            lat: 37.7694,
            lng: -122.4862,
            description: "Large urban park with gardens and museums",
            rating: 4.6,
          },
          {
            id: "6",
            name: "Pier 39",
            category: "entertainment",
            lat: 37.8087,
            lng: -122.4098,
            description: "Waterfront shopping and entertainment complex",
            rating: 4.2,
          },
        ],
        timestamp: new Date().toISOString(),
        center: args.center,
        zoom: args.zoom,
      },
    };
  },

  get_poi_details: async (args) => {
    await simulateDelay(400);
    const poiId = args.poi_id as string;
    return {
      structuredContent: {
        id: poiId,
        name: `Details for POI ${poiId}`,
        description: "Full detailed description with more information...",
        address: "123 Main St, San Francisco, CA 94102",
        hours: "9am - 10pm",
        phone: "(555) 123-4567",
        website: "https://example.com",
      },
    };
  },

  toggle_favorite: async (args) => {
    await simulateDelay(200);
    return {
      structuredContent: {
        poi_id: args.poi_id,
        is_favorite: args.is_favorite,
        timestamp: new Date().toISOString(),
      },
    };
  },
};

const defaultHandler: MockHandler = async (args) => {
  await simulateDelay(300);
  return {
    structuredContent: {
      success: true,
      message: "Mock response from Workbench",
      receivedArgs: args,
      timestamp: new Date().toISOString(),
    },
  };
};

function simulateDelay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function convertMockResponse(response: MockResponse): CallToolResponse {
  const result: CallToolResponse = {};

  if (response.structuredContent) {
    result.structuredContent = response.structuredContent;
  }
  if (response.content) {
    result.content = response.content;
  }
  if (response.isError) {
    result.isError = response.isError;
  }
  if (response._meta) {
    result._meta = response._meta;
  }

  return result;
}

export async function handleMockToolCall(
  toolName: string,
  args: Record<string, unknown>,
  mockConfig?: MockConfigState,
): Promise<MockToolCallResult> {
  if (mockConfig?.globalEnabled) {
    const toolConfig = mockConfig.tools[toolName];

    if (toolConfig?.activeVariantId) {
      const variant = toolConfig.variants.find(
        (v) => v.id === toolConfig.activeVariantId,
      );

      if (variant) {
        await simulateDelay(variant.delay);
        const response = convertMockResponse(variant.response);
        return {
          ...response,
          _mockVariant: variant.name,
        };
      }
    }
  }

  const handler = mockHandlers[toolName] || defaultHandler;
  return handler(args);
}

export function registerMockHandler(
  toolName: string,
  handler: MockHandler
): void {
  mockHandlers[toolName] = handler;
}

export function getAvailableMockTools(): string[] {
  return Object.keys(mockHandlers);
}


--- app/workbench/lib/openai-bridge.ts ---
import type { OpenAIGlobals } from "./types";

export function generateBridgeScript(initialGlobals: OpenAIGlobals): string {
  const globalsJson = JSON.stringify(initialGlobals);

  return `
(function() {
  'use strict';

  let globals = ${globalsJson};
  const pendingCalls = new Map();
  let callIdCounter = 0;

  function sendMethodCall(method, args) {
    return new Promise((resolve, reject) => {
      const id = String(++callIdCounter);
      pendingCalls.set(id, { resolve, reject });

      window.parent.postMessage({
        type: 'OPENAI_METHOD_CALL',
        id: id,
        method: method,
        args: args
      }, '*');

      // Timeout after 30 seconds
      setTimeout(() => {
        if (pendingCalls.has(id)) {
          pendingCalls.delete(id);
          reject(new Error('Method call timed out: ' + method));
        }
      }, 30000);
    });
  }

  window.addEventListener('message', function(event) {
    const data = event.data;
    if (!data || typeof data !== 'object') return;

    // Handle globals update from parent
    if (data.type === 'OPENAI_SET_GLOBALS') {
      const newGlobals = data.globals;
      const changedKeys = {};

      // Track which keys changed
      for (const key in newGlobals) {
        if (JSON.stringify(globals[key]) !== JSON.stringify(newGlobals[key])) {
          changedKeys[key] = newGlobals[key];
        }
      }

      // Update globals
      globals = { ...globals, ...newGlobals };

      // Dispatch event if anything changed
      if (Object.keys(changedKeys).length > 0) {
        window.dispatchEvent(new CustomEvent('openai:set_globals', {
          detail: { globals: changedKeys }
        }));
      }
    }

    // Handle method response from parent
    if (data.type === 'OPENAI_METHOD_RESPONSE') {
      const pending = pendingCalls.get(data.id);
      if (pending) {
        pendingCalls.delete(data.id);
        if (data.error) {
          pending.reject(new Error(data.error));
        } else {
          pending.resolve(data.result);
        }
      }
    }
  }, false);

  window.openai = {
    // Read-only globals (via getters)
    get theme() { return globals.theme; },
    get locale() { return globals.locale; },
    get displayMode() { return globals.displayMode; },
    get maxHeight() { return globals.maxHeight; },
    get toolInput() { return globals.toolInput; },
    get toolOutput() { return globals.toolOutput; },
    get toolResponseMetadata() { return globals.toolResponseMetadata; },
    get widgetState() { return globals.widgetState; },
    get userAgent() { return globals.userAgent; },
    get safeArea() { return globals.safeArea; },
    get view() { return globals.view; },

    // API Methods
    callTool: function(name, args) {
      return sendMethodCall('callTool', [name, args]);
    },

    setWidgetState: function(state) {
      // Update local state immediately
      globals.widgetState = { ...globals.widgetState, ...state };
      // Notify parent (fire-and-forget style, but still logged)
      sendMethodCall('setWidgetState', [state]).catch(function() {});
    },

    requestDisplayMode: function(opts) {
      return sendMethodCall('requestDisplayMode', [opts]);
    },

    sendFollowUpMessage: function(opts) {
      return sendMethodCall('sendFollowUpMessage', [opts]);
    },

    requestClose: function() {
      sendMethodCall('requestClose', []).catch(function() {});
    },

    openExternal: function(opts) {
      sendMethodCall('openExternal', [opts]).catch(function() {});
    },

    notifyIntrinsicHeight: function(height) {
      sendMethodCall('notifyIntrinsicHeight', [height]).catch(function() {});
    },

    requestModal: function(opts) {
      return sendMethodCall('requestModal', [opts]);
    },

    uploadFile: function(file) {
      return sendMethodCall('uploadFile', [file]);
    },

    getFileDownloadUrl: function(opts) {
      return sendMethodCall('getFileDownloadUrl', [opts]);
    }
  };

  // Make it non-configurable to match real ChatGPT behavior
  Object.defineProperty(window, 'openai', {
    configurable: false,
    writable: false
  });

  // Signal that the bridge is ready
  console.log('[Workbench] window.openai bridge initialized');
})();
`;
}

export function generateComponentBundle(
  bridgeScript: string,
  componentHtml: string,
  theme: "light" | "dark" = "light",
): string {
  const themeClass = theme === "dark" ? "dark" : "";

  return `<!DOCTYPE html>
<html lang="en" class="${themeClass}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Workbench Component</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    tailwind.config = {
      darkMode: 'class',
      theme: {
        extend: {
          colors: {
            background: 'hsl(var(--background, 0 0% 100%))',
            foreground: 'hsl(var(--foreground, 222.2 47.4% 11.2%))',
            muted: 'hsl(var(--muted, 210 40% 96.1%))',
            'muted-foreground': 'hsl(var(--muted-foreground, 215.4 16.3% 46.9%))',
            primary: 'hsl(var(--primary, 222.2 47.4% 11.2%))',
            'primary-foreground': 'hsl(var(--primary-foreground, 210 40% 98%))',
          }
        }
      }
    }
  </script>
  <style>
    :root {
      --background: 0 0% 100%;
      --foreground: 222.2 47.4% 11.2%;
      --muted: 210 40% 96.1%;
      --muted-foreground: 215.4 16.3% 46.9%;
      --primary: 222.2 47.4% 11.2%;
      --primary-foreground: 210 40% 98%;
    }
    .dark {
      --background: 224 71% 4%;
      --foreground: 213 31% 91%;
      --muted: 223 47% 11%;
      --muted-foreground: 215.4 16.3% 56.9%;
      --primary: 210 40% 98%;
      --primary-foreground: 222.2 47.4% 1.2%;
    }
    body {
      background-color: hsl(var(--background));
      color: hsl(var(--foreground));
      font-family: system-ui, -apple-system, sans-serif;
      margin: 0;
      padding: 16px;
    }
  </style>
  <script>${bridgeScript}</script>
</head>
<body>
  <div id="root">${componentHtml}</div>
</body>
</html>`;
}


--- app/workbench/lib/store.ts ---
"use client";

import { create } from "zustand";
import { useMemo } from "react";
import type {
  DisplayMode,
  Theme,
  DeviceType,
  ConsoleEntry,
  ConsoleEntryType,
  OpenAIGlobals,
  SafeAreaInsets,
  View,
  WidgetState,
  UserLocation,
} from "./types";
import { DEVICE_PRESETS } from "./types";
import { workbenchComponents } from "./component-registry";
import { clearFiles } from "./file-store";
import type {
  MockConfigState,
  MockVariant,
  ToolAnnotations,
  ToolDescriptorMeta,
  ToolSchemas,
} from "./mock-config";
import { createToolMockConfig, createEmptyMockConfigState } from "./mock-config";

const defaultComponent = workbenchComponents[0];

export type ActiveJsonTab =
  | "toolInput"
  | "toolOutput"
  | "widgetState"
  | "toolResponseMetadata"
  | "window"
  | "mocks";

interface WorkbenchState {
  selectedComponent: string;
  displayMode: DisplayMode;
  theme: Theme;
  locale: string;
  deviceType: DeviceType;
  toolInput: Record<string, unknown>;
  toolOutput: Record<string, unknown> | null;
  widgetState: WidgetState;
  maxHeight: number;
  toolResponseMetadata: Record<string, unknown> | null;
  safeAreaInsets: SafeAreaInsets;
  consoleLogs: ConsoleEntry[];
  collapsedSections: Record<string, boolean>;
  activeJsonTab: ActiveJsonTab;
  isTransitioning: boolean;
  transitionFrom: DisplayMode | null;
  view: View | null;
  mockConfig: MockConfigState;
  userLocation: UserLocation | null;
  isWidgetClosed: boolean;
  widgetSessionId: string;

  setSelectedComponent: (id: string) => void;
  setDisplayMode: (mode: DisplayMode) => void;
  setTransitioning: (transitioning: boolean) => void;
  setTheme: (theme: Theme) => void;
  setLocale: (locale: string) => void;
  setDeviceType: (type: DeviceType) => void;
  setToolInput: (input: Record<string, unknown>) => void;
  setToolOutput: (output: Record<string, unknown> | null) => void;
  setWidgetState: (state: WidgetState) => void;
  updateWidgetState: (state: Record<string, unknown>) => void;
  setMaxHeight: (height: number) => void;
  setToolResponseMetadata: (metadata: Record<string, unknown> | null) => void;
  setSafeAreaInsets: (insets: Partial<SafeAreaInsets>) => void;
  addConsoleEntry: (entry: {
    type: ConsoleEntryType;
    method: string;
    args?: unknown;
    result?: unknown;
  }) => void;
  clearConsole: () => void;
  restoreConsoleLogs: (entries: ConsoleEntry[]) => void;
  toggleSection: (section: string) => void;
  setActiveJsonTab: (tab: ActiveJsonTab) => void;
  setView: (view: View | null) => void;
  getOpenAIGlobals: () => OpenAIGlobals;
  setUserLocation: (location: UserLocation | null) => void;
  setWidgetClosed: (closed: boolean) => void;

  setMocksEnabled: (enabled: boolean) => void;
  registerTool: (toolName: string) => void;
  removeTool: (toolName: string) => void;
  setActiveVariant: (toolName: string, variantId: string | null) => void;
  setInterceptMode: (toolName: string, enabled: boolean) => void;
  addVariant: (toolName: string, variant: MockVariant) => void;
  updateVariant: (
    toolName: string,
    variantId: string,
    updates: Partial<MockVariant>,
  ) => void;
  removeVariant: (toolName: string, variantId: string) => void;
  setMockConfig: (config: MockConfigState) => void;
  setToolAnnotations: (toolName: string, annotations: ToolAnnotations) => void;
  setToolDescriptorMeta: (
    toolName: string,
    meta: ToolDescriptorMeta,
  ) => void;
  setToolSchemas: (toolName: string, schemas: ToolSchemas) => void;
}

function buildOpenAIGlobals(
  state: Pick<
    WorkbenchState,
    | "theme"
    | "locale"
    | "displayMode"
    | "maxHeight"
    | "toolInput"
    | "toolOutput"
    | "toolResponseMetadata"
    | "widgetState"
    | "deviceType"
    | "safeAreaInsets"
    | "view"
    | "userLocation"
  >,
): OpenAIGlobals {
  const preset = DEVICE_PRESETS[state.deviceType];

  return {
    theme: state.theme,
    locale: state.locale,
    displayMode: state.displayMode,
    maxHeight: state.maxHeight,
    toolInput: state.toolInput,
    toolOutput: state.toolOutput,
    toolResponseMetadata: state.toolResponseMetadata,
    widgetState: state.widgetState,
    userAgent: preset.userAgent,
    safeArea: {
      insets: state.safeAreaInsets,
    },
    view: state.view,
    userLocation: state.userLocation,
  };
}

export const useWorkbenchStore = create<WorkbenchState>((set, get) => ({
  selectedComponent: defaultComponent?.id ?? "chart",
  displayMode: "inline",
  theme: "light",
  locale: "en-US",
  deviceType: "desktop",
  toolInput: defaultComponent?.defaultProps ?? {},
  toolOutput: null,
  widgetState: null,
  maxHeight: 500,
  toolResponseMetadata: null,
  safeAreaInsets: { top: 10, bottom: 100, left: 10, right: 10 },
  consoleLogs: [],
  collapsedSections: {},
  activeJsonTab: "toolInput",
  isTransitioning: false,
  transitionFrom: null,
  view: null,
  mockConfig: createEmptyMockConfigState(),
  userLocation: null,
  isWidgetClosed: false,
  widgetSessionId: crypto.randomUUID(),
  setSelectedComponent: (id) => {
    clearFiles();
    set(() => {
      const entry = workbenchComponents.find((comp) => comp.id === id) ?? null;

      return {
        selectedComponent: id,
        toolInput: entry?.defaultProps ?? {},
        toolOutput: null,
        widgetState: null,
        toolResponseMetadata: null,
        activeJsonTab: "toolInput",
        isWidgetClosed: false,
        widgetSessionId: crypto.randomUUID(),
      };
    });
  },
  setDisplayMode: (mode) => set(() => ({ displayMode: mode })),
  setTransitioning: (transitioning) =>
    set((state) => ({
      isTransitioning: transitioning,
      transitionFrom: transitioning ? state.displayMode : null,
    })),
  setTheme: (theme) => set(() => ({ theme })),
  setLocale: (locale) => set(() => ({ locale })),
  setDeviceType: (type) => set(() => ({ deviceType: type })),
  setToolInput: (input) => set(() => ({ toolInput: input })),
  setToolOutput: (output) => set(() => ({ toolOutput: output })),
  setWidgetState: (state) => set(() => ({ widgetState: state })),
  updateWidgetState: (state) =>
    set((prev) => ({
      widgetState: { ...(prev.widgetState ?? {}), ...state },
    })),
  setMaxHeight: (height) => set(() => ({ maxHeight: height })),
  setToolResponseMetadata: (metadata) =>
    set(() => ({ toolResponseMetadata: metadata })),
  setSafeAreaInsets: (insets) =>
    set((prev) => ({
      safeAreaInsets: { ...prev.safeAreaInsets, ...insets },
    })),
  addConsoleEntry: (entry) =>
    set((state) => ({
      consoleLogs: [
        ...state.consoleLogs,
        {
          ...entry,
          id: crypto.randomUUID(),
          timestamp: new Date(),
        },
      ],
    })),
  clearConsole: () => set(() => ({ consoleLogs: [] })),
  restoreConsoleLogs: (entries) => set(() => ({ consoleLogs: entries })),
  toggleSection: (section) =>
    set((state) => ({
      collapsedSections: {
        ...state.collapsedSections,
        [section]: !state.collapsedSections[section],
      },
    })),
  setActiveJsonTab: (tab) => set(() => ({ activeJsonTab: tab })),
  setView: (view) => set(() => ({ view })),
  getOpenAIGlobals: () => {
    const state = get();
    return buildOpenAIGlobals(state);
  },
  setUserLocation: (location) => set(() => ({ userLocation: location })),
  setWidgetClosed: (closed) => set(() => ({ isWidgetClosed: closed })),

  setMocksEnabled: (enabled) =>
    set((state) => ({
      mockConfig: { ...state.mockConfig, globalEnabled: enabled },
    })),

  registerTool: (toolName) =>
    set((state) => {
      if (state.mockConfig.tools[toolName]) {
        return state;
      }
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: createToolMockConfig(toolName),
          },
        },
      };
    }),

  removeTool: (toolName) =>
    set((state) => {
      const { [toolName]: _, ...remainingTools } = state.mockConfig.tools;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: remainingTools,
        },
      };
    }),

  setActiveVariant: (toolName, variantId) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: { ...tool, activeVariantId: variantId },
          },
        },
      };
    }),

  setInterceptMode: (toolName, enabled) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: { ...tool, interceptMode: enabled },
          },
        },
      };
    }),

  addVariant: (toolName, variant) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: {
              ...tool,
              variants: [...tool.variants, variant],
            },
          },
        },
      };
    }),

  updateVariant: (toolName, variantId, updates) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: {
              ...tool,
              variants: tool.variants.map((v) =>
                v.id === variantId ? { ...v, ...updates } : v,
              ),
            },
          },
        },
      };
    }),

  removeVariant: (toolName, variantId) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      const newActiveId =
        tool.activeVariantId === variantId ? null : tool.activeVariantId;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: {
              ...tool,
              activeVariantId: newActiveId,
              variants: tool.variants.filter((v) => v.id !== variantId),
            },
          },
        },
      };
    }),

  setMockConfig: (config) => set(() => ({ mockConfig: config })),

  setToolAnnotations: (toolName, annotations) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: { ...tool, annotations },
          },
        },
      };
    }),

  setToolDescriptorMeta: (toolName, meta) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: { ...tool, descriptorMeta: meta },
          },
        },
      };
    }),

  setToolSchemas: (toolName, schemas) =>
    set((state) => {
      const tool = state.mockConfig.tools[toolName];
      if (!tool) return state;
      return {
        mockConfig: {
          ...state.mockConfig,
          tools: {
            ...state.mockConfig.tools,
            [toolName]: { ...tool, schemas },
          },
        },
      };
    }),
}));

export const useSelectedComponent = () =>
  useWorkbenchStore((s) => s.selectedComponent);
export const useDisplayMode = () => useWorkbenchStore((s) => s.displayMode);
export const useIsTransitioning = () =>
  useWorkbenchStore((s) => s.isTransitioning);
export const useTransitionFrom = () =>
  useWorkbenchStore((s) => s.transitionFrom);
export const useWorkbenchTheme = () => useWorkbenchStore((s) => s.theme);
export const useDeviceType = () => useWorkbenchStore((s) => s.deviceType);
export const useConsoleLogs = () => useWorkbenchStore((s) => s.consoleLogs);
export const useClearConsole = () => useWorkbenchStore((s) => s.clearConsole);
export const useToolInput = () => useWorkbenchStore((s) => s.toolInput);
export const useToolOutput = () => useWorkbenchStore((s) => s.toolOutput);
export const useActiveJsonTab = () => useWorkbenchStore((s) => s.activeJsonTab);
export const useMockConfig = () => useWorkbenchStore((s) => s.mockConfig);

export const useOpenAIGlobals = (): OpenAIGlobals => {
  const theme = useWorkbenchStore((s) => s.theme);
  const locale = useWorkbenchStore((s) => s.locale);
  const displayMode = useWorkbenchStore((s) => s.displayMode);
  const maxHeight = useWorkbenchStore((s) => s.maxHeight);
  const toolInput = useWorkbenchStore((s) => s.toolInput);
  const toolOutput = useWorkbenchStore((s) => s.toolOutput);
  const toolResponseMetadata = useWorkbenchStore((s) => s.toolResponseMetadata);
  const widgetState = useWorkbenchStore((s) => s.widgetState);
  const deviceType = useWorkbenchStore((s) => s.deviceType);
  const safeAreaInsets = useWorkbenchStore((s) => s.safeAreaInsets);
  const view = useWorkbenchStore((s) => s.view);
  const userLocation = useWorkbenchStore((s) => s.userLocation);

  return useMemo(
    () =>
      buildOpenAIGlobals({
        theme,
        locale,
        displayMode,
        maxHeight,
        toolInput,
        toolOutput,
        toolResponseMetadata,
        widgetState,
        deviceType,
        safeAreaInsets,
        view,
        userLocation,
      }),
    [
      theme,
      locale,
      displayMode,
      maxHeight,
      toolInput,
      toolOutput,
      toolResponseMetadata,
      widgetState,
      deviceType,
      safeAreaInsets,
      view,
      userLocation,
    ],
  );
};

export const useIsWidgetClosed = () =>
  useWorkbenchStore((s) => s.isWidgetClosed);
export const useWidgetSessionId = () =>
  useWorkbenchStore((s) => s.widgetSessionId);


--- app/workbench/lib/store.test.ts ---
import { describe, it, expect, beforeEach } from "vitest";
import { useWorkbenchStore } from "./store";
import { workbenchComponents } from "./component-registry";

describe("Workbench Store", () => {
  // Reset store before each test
  beforeEach(() => {
    const store = useWorkbenchStore.getState();
    // Reset to initial state by selecting the first component
    const defaultComponent = workbenchComponents[0];
    store.setSelectedComponent(defaultComponent?.id ?? "chart");
  });

  describe("setSelectedComponent", () => {
    it("should update selectedComponent to the given id", () => {
      const store = useWorkbenchStore.getState();
      const targetComponent = workbenchComponents[1]; // Pick second component

      if (!targetComponent) {
        throw new Error("Test requires at least 2 components in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().selectedComponent).toBe(targetComponent.id);
    });

    it("should reset toolInput to component's defaultProps", () => {
      const store = useWorkbenchStore.getState();
      const targetComponent = workbenchComponents[1];

      if (!targetComponent) {
        throw new Error("Test requires at least 2 components in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().toolInput).toEqual(targetComponent.defaultProps);
    });

    it("should reset toolOutput to null", () => {
      const store = useWorkbenchStore.getState();

      // First set toolOutput to something non-null
      store.setToolOutput({ someKey: "someValue" });
      expect(useWorkbenchStore.getState().toolOutput).not.toBeNull();

      // Switch component - should reset to null
      const targetComponent = workbenchComponents[0];
      if (!targetComponent) {
        throw new Error("Test requires at least 1 component in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().toolOutput).toBeNull();
    });

    it("should reset widgetState to null", () => {
      const store = useWorkbenchStore.getState();

      // First set widgetState to something non-null
      store.setWidgetState({ someKey: "someValue" });
      expect(useWorkbenchStore.getState().widgetState).not.toBeNull();

      // Switch component - should reset to null
      const targetComponent = workbenchComponents[0];
      if (!targetComponent) {
        throw new Error("Test requires at least 1 component in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().widgetState).toBeNull();
    });

    it("should reset toolResponseMetadata to null", () => {
      const store = useWorkbenchStore.getState();

      // First set toolResponseMetadata to something non-null
      store.setToolResponseMetadata({ someKey: "someValue" });
      expect(useWorkbenchStore.getState().toolResponseMetadata).not.toBeNull();

      // Switch component - should reset to null
      const targetComponent = workbenchComponents[0];
      if (!targetComponent) {
        throw new Error("Test requires at least 1 component in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().toolResponseMetadata).toBeNull();
    });

    it("should reset activeJsonTab to 'toolInput'", () => {
      const store = useWorkbenchStore.getState();

      // First set activeJsonTab to something else
      store.setActiveJsonTab("toolOutput");
      expect(useWorkbenchStore.getState().activeJsonTab).toBe("toolOutput");

      // Switch component - should reset to toolInput
      const targetComponent = workbenchComponents[0];
      if (!targetComponent) {
        throw new Error("Test requires at least 1 component in registry");
      }

      store.setSelectedComponent(targetComponent.id);

      expect(useWorkbenchStore.getState().activeJsonTab).toBe("toolInput");
    });

    it("should handle non-existent component gracefully", () => {
      const store = useWorkbenchStore.getState();

      store.setSelectedComponent("non-existent-component-id");

      expect(useWorkbenchStore.getState().selectedComponent).toBe("non-existent-component-id");
      expect(useWorkbenchStore.getState().toolInput).toEqual({});
      expect(useWorkbenchStore.getState().toolOutput).toBeNull();
      expect(useWorkbenchStore.getState().widgetState).toBeNull();
      expect(useWorkbenchStore.getState().toolResponseMetadata).toBeNull();
    });
  });

  describe("getOpenAIGlobals", () => {
    it("should return correct theme", () => {
      const store = useWorkbenchStore.getState();

      store.setTheme("dark");
      const globals = store.getOpenAIGlobals();

      expect(globals.theme).toBe("dark");
    });

    it("should return correct locale", () => {
      const store = useWorkbenchStore.getState();

      store.setLocale("es-ES");
      const globals = store.getOpenAIGlobals();

      expect(globals.locale).toBe("es-ES");
    });

    it("should return correct displayMode", () => {
      const store = useWorkbenchStore.getState();

      store.setDisplayMode("fullscreen");
      const globals = store.getOpenAIGlobals();

      expect(globals.displayMode).toBe("fullscreen");
    });

    it("should return correct maxHeight", () => {
      const store = useWorkbenchStore.getState();

      store.setMaxHeight(1200);
      const globals = store.getOpenAIGlobals();

      expect(globals.maxHeight).toBe(1200);
    });

    it("should include toolInput", () => {
      const store = useWorkbenchStore.getState();
      const testInput = { testKey: "testValue" };

      store.setToolInput(testInput);
      const globals = store.getOpenAIGlobals();

      expect(globals.toolInput).toEqual(testInput);
    });

    it("should include toolOutput (null when unset)", () => {
      const store = useWorkbenchStore.getState();

      store.setToolOutput(null);
      const globals = store.getOpenAIGlobals();

      expect(globals.toolOutput).toBeNull();
    });

    it("should include toolOutput (object when set)", () => {
      const store = useWorkbenchStore.getState();
      const testOutput = { resultKey: "resultValue" };

      store.setToolOutput(testOutput);
      const globals = store.getOpenAIGlobals();

      expect(globals.toolOutput).toEqual(testOutput);
    });

    it("should include widgetState", () => {
      const store = useWorkbenchStore.getState();
      const testState = { stateKey: "stateValue" };

      store.setWidgetState(testState);
      const globals = store.getOpenAIGlobals();

      expect(globals.widgetState).toEqual(testState);
    });

    it("should include toolResponseMetadata", () => {
      const store = useWorkbenchStore.getState();
      const testMetadata = { metaKey: "metaValue" };

      store.setToolResponseMetadata(testMetadata);
      const globals = store.getOpenAIGlobals();

      expect(globals.toolResponseMetadata).toEqual(testMetadata);
    });

    it("should include safeAreaInsets", () => {
      const store = useWorkbenchStore.getState();
      const testInsets = { top: 10, bottom: 20, left: 5, right: 5 };

      store.setSafeAreaInsets(testInsets);
      const globals = store.getOpenAIGlobals();

      expect(globals.safeArea.insets).toEqual(testInsets);
    });

    it("should include userAgent with device type", () => {
      const store = useWorkbenchStore.getState();

      store.setDeviceType("mobile");
      const globals = store.getOpenAIGlobals();

      expect(globals.userAgent.device.type).toBe("mobile");
    });

    it("should include userAgent capabilities for mobile (no hover, touch)", () => {
      const store = useWorkbenchStore.getState();

      store.setDeviceType("mobile");
      const globals = store.getOpenAIGlobals();

      expect(globals.userAgent.capabilities.hover).toBe(false);
      expect(globals.userAgent.capabilities.touch).toBe(true);
    });

    it("should include userAgent capabilities for desktop (hover, no touch)", () => {
      const store = useWorkbenchStore.getState();

      store.setDeviceType("desktop");
      const globals = store.getOpenAIGlobals();

      expect(globals.userAgent.capabilities.hover).toBe(true);
      expect(globals.userAgent.capabilities.touch).toBe(false);
    });
  });

  describe("activeJsonTab", () => {
    it("should default to 'toolInput'", () => {
      const state = useWorkbenchStore.getState();

      expect(state.activeJsonTab).toBe("toolInput");
    });

    it("should update when setActiveJsonTab is called", () => {
      const store = useWorkbenchStore.getState();

      store.setActiveJsonTab("toolOutput");
      expect(useWorkbenchStore.getState().activeJsonTab).toBe("toolOutput");

      store.setActiveJsonTab("widgetState");
      expect(useWorkbenchStore.getState().activeJsonTab).toBe("widgetState");

      store.setActiveJsonTab("toolResponseMetadata");
      expect(useWorkbenchStore.getState().activeJsonTab).toBe("toolResponseMetadata");

      store.setActiveJsonTab("toolInput");
      expect(useWorkbenchStore.getState().activeJsonTab).toBe("toolInput");
    });
  });

  describe("setDeviceType", () => {
    it("should update deviceType without changing maxHeight", () => {
      const store = useWorkbenchStore.getState();
      const initialMaxHeight = store.maxHeight;

      store.setDeviceType("mobile");
      const state = useWorkbenchStore.getState();

      expect(state.deviceType).toBe("mobile");
      expect(state.maxHeight).toBe(initialMaxHeight);
    });

    it("should update deviceType to tablet without changing maxHeight", () => {
      const store = useWorkbenchStore.getState();
      store.setMaxHeight(500);

      store.setDeviceType("tablet");
      const state = useWorkbenchStore.getState();

      expect(state.deviceType).toBe("tablet");
      expect(state.maxHeight).toBe(500);
    });

    it("should update deviceType to desktop without changing maxHeight", () => {
      const store = useWorkbenchStore.getState();
      store.setMaxHeight(600);

      store.setDeviceType("desktop");
      const state = useWorkbenchStore.getState();

      expect(state.deviceType).toBe("desktop");
      expect(state.maxHeight).toBe(600);
    });
  });

  describe("OpenAI Globals Consistency", () => {
    it("should reflect all state changes in getOpenAIGlobals", () => {
      const store = useWorkbenchStore.getState();

      // Set various state values
      store.setTheme("dark");
      store.setLocale("fr-FR");
      store.setDisplayMode("pip");
      store.setDeviceType("tablet");
      store.setMaxHeight(1500);
      store.setToolInput({ input1: "value1" });
      store.setToolOutput({ output1: "result1" });
      store.setWidgetState({ state1: "stateValue1" });
      store.setToolResponseMetadata({ meta1: "metaValue1" });
      store.setSafeAreaInsets({ top: 15, bottom: 25, left: 10, right: 10 });

      const globals = store.getOpenAIGlobals();

      // Verify all values are correctly reflected
      expect(globals.theme).toBe("dark");
      expect(globals.locale).toBe("fr-FR");
      expect(globals.displayMode).toBe("pip");
      expect(globals.maxHeight).toBe(1500);
      expect(globals.toolInput).toEqual({ input1: "value1" });
      expect(globals.toolOutput).toEqual({ output1: "result1" });
      expect(globals.widgetState).toEqual({ state1: "stateValue1" });
      expect(globals.toolResponseMetadata).toEqual({ meta1: "metaValue1" });
      expect(globals.safeArea.insets).toEqual({ top: 15, bottom: 25, left: 10, right: 10 });
      expect(globals.userAgent.device.type).toBe("tablet");
    });

    it("should maintain consistent structure across multiple calls", () => {
      const store = useWorkbenchStore.getState();

      const globals1 = store.getOpenAIGlobals();
      const globals2 = store.getOpenAIGlobals();

      // Should have identical structure
      expect(globals1).toEqual(globals2);
      expect(Object.keys(globals1).sort()).toEqual(Object.keys(globals2).sort());
    });

    it("should handle null values correctly in globals", () => {
      const store = useWorkbenchStore.getState();

      // Set all nullable fields to null
      store.setToolOutput(null);
      store.setWidgetState(null);
      store.setToolResponseMetadata(null);

      const globals = store.getOpenAIGlobals();

      expect(globals.toolOutput).toBeNull();
      expect(globals.widgetState).toBeNull();
      expect(globals.toolResponseMetadata).toBeNull();
    });

    it("should update globals when safeAreaInsets are partially updated", () => {
      const store = useWorkbenchStore.getState();

      // Set initial insets
      store.setSafeAreaInsets({ top: 10, bottom: 10, left: 10, right: 10 });

      // Partially update (should merge with existing)
      store.setSafeAreaInsets({ top: 20 });

      const globals = store.getOpenAIGlobals();

      expect(globals.safeArea.insets.top).toBe(20);
      expect(globals.safeArea.insets.bottom).toBe(10);
      expect(globals.safeArea.insets.left).toBe(10);
      expect(globals.safeArea.insets.right).toBe(10);
    });
  });
});


--- app/workbench/components/styles.ts ---
export const PANEL_TOGGLE_CLASSES =
  "text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors";

export const CONTROL_BG_CLASSES =
  "bg-input/70 hover:bg-input/80 dark:bg-input/70! dark:hover:bg-input/80!";

export const TRANSPARENT_CONTROL_BG_CLASSES =
  "bg-transparent! dark:bg-transparent! hover:bg-input/70! dark:hover:bg-input/70!";

export const INPUT_GROUP_CLASSES = `${CONTROL_BG_CLASSES} w-fit border-none! shadow-none! max-h-7 tabular-nums`;

export const INPUT_CLASSES =
  "bg-transparent! dark:bg-transparent! h-7! focus:ring-0! border-0! border-none! [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none text-xs!";

export const ADDON_CLASSES = "bg-transparent! text-sm font-normal select-none";

export const COMPACT_ADDON_CLASSES =
  "bg-transparent! text-xs font-normal select-none";

export const SELECT_CLASSES = `${TRANSPARENT_CONTROL_BG_CLASSES} w-fit text-xs shadow-none h-7! border-none select-none pl-2 pr-1`;

export const TOGGLE_BUTTON_CLASSES = `${CONTROL_BG_CLASSES} flex-1 gap-2 border-none text-xs font-light h-7! select-none`;

export const TOGGLE_BUTTON_ACTIVE_CLASSES = ` flex-1 gap-2 border-none text-xs font-light h-7! select-none bg-primary/20! hover:bg-primary/20!`;

export const INFO_BOX_CLASSES = `${CONTROL_BG_CLASSES} text-muted-foreground rounded-md p-2 text-xs`;

export const LABEL_CLASSES =
  "text-sm font-normal text-neutral-500 dark:text-neutral-400";

export const COMPACT_SMALL_TEXT_CLASSES = "text-xs font-normal";

export const COMPACT_LABEL_CLASSES =
  "text-xs font-normal text-neutral-500 dark:text-neutral-400";

export const TAB_TRIGGER_CLASSES = "bg-transparent font-normal";


--- app/workbench/lib/transition-config.ts ---
export const MORPH_TIMING = {
  viewTransitionDuration: 350,
} as const;

export const VIEW_TRANSITION_NAME = "workbench-app-container";
export const VIEW_TRANSITION_PARENT_NAME = "workbench-preview-panel";
export const VIEW_TRANSITION_ROOT_NAME = "workbench-shell";


--- components/tool-ui/shared/actions-config.ts ---
import type { Action, ActionsConfig } from "./schema";

export type ActionsProp = ActionsConfig | Action[];

export function normalizeActionsConfig(
  actions?: ActionsProp,
): ActionsConfig | null {
  if (!actions) return null;

  const resolved = Array.isArray(actions)
    ? { items: actions }
    : {
        items: actions.items ?? [],
        align: actions.align,
        confirmTimeout: actions.confirmTimeout,
      };

  if (!resolved.items || resolved.items.length === 0) {
    return null;
  }

  return resolved;
}


--- components/tool-ui/audio/index.ts ---
export { Audio } from "./audio";
export type { AudioProps } from "./audio";
export { AudioErrorBoundary } from "./error-boundary";
export { AudioProvider, useAudio } from "./context";
export type { AudioPlaybackState, AudioContextValue } from "./context";
export { SerializableAudioSchema, parseSerializableAudio } from "./schema";
export type { SerializableAudio, Source } from "./schema";


--- components/tool-ui/image/index.ts ---
export { Image } from "./image";
export type { ImageProps } from "./image";
export { ImageErrorBoundary } from "./error-boundary";
export { SerializableImageSchema, parseSerializableImage } from "./schema";
export type { SerializableImage, Source } from "./schema";


--- components/tool-ui/instagram-post/index.ts ---
export { InstagramPost } from "./instagram-post";
export type { InstagramPostProps } from "./instagram-post";
export { InstagramPostErrorBoundary } from "./error-boundary";
export {
  SerializableInstagramPostSchema,
  parseSerializableInstagramPost,
} from "./schema";
export type {
  InstagramPostData,
  InstagramPostAuthor,
  InstagramPostMedia,
  InstagramPostStats,
} from "./schema";


--- components/tool-ui/link-preview/index.ts ---
export { LinkPreview } from "./link-preview";
export type { LinkPreviewProps } from "./link-preview";
export { LinkPreviewErrorBoundary } from "./error-boundary";
export { SerializableLinkPreviewSchema, parseSerializableLinkPreview } from "./schema";
export type { SerializableLinkPreview } from "./schema";


--- components/tool-ui/linkedin-post/index.ts ---
export { LinkedInPost } from "./linkedin-post";
export type { LinkedInPostProps } from "./linkedin-post";
export { LinkedInPostErrorBoundary } from "./error-boundary";
export {
  SerializableLinkedInPostSchema,
  parseSerializableLinkedInPost,
} from "./schema";
export type {
  LinkedInPostData,
  LinkedInPostAuthor,
  LinkedInPostMedia,
  LinkedInPostLinkPreview,
  LinkedInPostStats,
} from "./schema";


--- components/tool-ui/shared/index.ts ---
export { ActionButtons } from "./action-buttons";
export type { ActionButtonsProps } from "./action-buttons";
export { normalizeActionsConfig, type ActionsProp } from "./actions-config";
export * from "./error-boundary";
export * from "./parse";
export * from "./schema";
export * from "./use-copy-to-clipboard";
export * from "./utils";


--- components/tool-ui/video/index.ts ---
export { Video } from "./video";
export type { VideoProps } from "./video";
export { VideoErrorBoundary } from "./error-boundary";
export { VideoProvider, useVideo } from "./context";
export type { VideoPlaybackState, VideoContextValue } from "./context";
export { SerializableVideoSchema, parseSerializableVideo } from "./schema";
export type { SerializableVideo, Source } from "./schema";


--- components/tool-ui/x-post/index.ts ---
export { XPost } from "./x-post";
export type { XPostProps } from "./x-post";
export { XPostErrorBoundary } from "./error-boundary";
export { SerializableXPostSchema, parseSerializableXPost } from "./schema";
export type {
  XPostData,
  XPostAuthor,
  XPostMedia,
  XPostLinkPreview,
  XPostStats,
} from "./schema";


--- components/tool-ui/shared/parse.ts ---
import { z } from "zod";

function formatZodPath(path: Array<string | number | symbol>): string {
  if (path.length === 0) return "root";
  return path
    .map((segment) =>
      typeof segment === "number" ? `[${segment}]` : String(segment),
    )
    .join(".");
}

/**
 * Format Zod errors into a compact `path: message` string.
 */
export function formatZodError(error: z.ZodError): string {
  const parts = error.issues.map((issue) => {
    const path = formatZodPath(issue.path);
    return `${path}: ${issue.message}`;
  });

  return Array.from(new Set(parts)).join("; ");
}

/**
 * Parse unknown input and throw a readable error.
 */
export function parseWithSchema<T>(
  schema: z.ZodType<T>,
  input: unknown,
  name: string,
): T {
  const res = schema.safeParse(input);
  if (!res.success) {
    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);
  }
  return res.data;
}


--- hooks/use-mobile.ts ---
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}


--- hooks/use-preset-param.ts ---
"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";

interface UsePresetParamOptions<T extends string> {
  presets: Record<T, unknown>;
  defaultPreset: T;
  paramName?: string;
}

interface UsePresetParamReturn<T extends string> {
  currentPreset: T;
  setPreset: (preset: T) => void;
}

export function usePresetParam<T extends string>({
  presets,
  defaultPreset,
  paramName = "preset",
}: UsePresetParamOptions<T>): UsePresetParamReturn<T> {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const presetKeysRef = useRef<Set<string>>(new Set(Object.keys(presets)));

  const currentParamValue = useMemo(
    () => searchParams.get(paramName),
    [searchParams, paramName],
  );

  const isValidPreset = useCallback(
    (value: string | null): value is T => {
      return value !== null && presetKeysRef.current.has(value);
    },
    [],
  );

  const initialPreset = isValidPreset(currentParamValue)
    ? currentParamValue
    : defaultPreset;

  const [currentPreset, setCurrentPresetState] = useState<T>(initialPreset);

  useEffect(() => {
    if (isValidPreset(currentParamValue)) {
      setCurrentPresetState(currentParamValue);
    }
  }, [currentParamValue, isValidPreset]);

  const setPreset = useCallback(
    (preset: T) => {
      setCurrentPresetState(preset);

      if (currentParamValue === preset) return;

      const params = new URLSearchParams(searchParams.toString());
      params.set(paramName, preset);
      router.push(`${pathname}?${params.toString()}`, { scroll: false });
    },
    [router, pathname, searchParams, paramName, currentParamValue],
  );

  return { currentPreset, setPreset };
}


--- hooks/use-reduced-motion.ts ---
"use client";

import { useState, useEffect } from "react";

export function useReducedMotion(): boolean {
  const [reducedMotion, setReducedMotion] = useState(false);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReducedMotion(mediaQuery.matches);

    const handleChange = () => setReducedMotion(mediaQuery.matches);
    mediaQuery.addEventListener?.("change", handleChange);
    return () => mediaQuery.removeEventListener?.("change", handleChange);
  }, []);

  return reducedMotion;
}


--- hooks/use-responsive-preview.ts ---
"use client";

import { useCallback, useRef } from "react";
import type { ImperativePanelGroupHandle } from "react-resizable-panels";

interface UseResponsivePreviewOptions {
  minWidth: number;
  maxWidth: number;
  tolerance?: number;
}

interface UseResponsivePreviewReturn {
  panelGroupRef: React.RefObject<ImperativePanelGroupHandle | null>;
  handleLayout: (sizes: number[]) => void;
}

type PreviewLayoutSizes = [
  leftGutter: number,
  previewWidth: number,
  rightGutter: number,
];

const layoutMatchesTarget = (
  actual: number[],
  target: number[],
  tolerance: number,
): boolean => {
  if (actual.length !== target.length) return false;
  return actual.every((val, i) => Math.abs(val - target[i]) < tolerance);
};

export function useResponsivePreview({
  minWidth,
  maxWidth,
  tolerance = 0.5,
}: UseResponsivePreviewOptions): UseResponsivePreviewReturn {
  const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null);
  const isSyncing = useRef(false);

  const handleLayout = useCallback(
    (sizes: number[]) => {
      if (!panelGroupRef.current) return;

      if (isSyncing.current) {
        isSyncing.current = false;
        return;
      }

      if (sizes.length !== 3) return;
      const [, previewWidth] = sizes as PreviewLayoutSizes;

      const clampedWidth = Math.min(maxWidth, Math.max(minWidth, previewWidth));
      const gutter = Math.max(0, (100 - clampedWidth) / 2);
      const targetLayout: PreviewLayoutSizes = [gutter, clampedWidth, gutter];

      if (!layoutMatchesTarget(sizes, targetLayout, tolerance)) {
        isSyncing.current = true;
        panelGroupRef.current.setLayout(targetLayout);
      }
    },
    [minWidth, maxWidth, tolerance],
  );

  return { panelGroupRef, handleLayout };
}


--- hooks/use-tab-search-param.ts ---
"use client";

import { useCallback, useEffect, useRef, type RefObject } from "react";
import { useQueryState } from "nuqs";

interface UseTabSearchParamOptions<T extends string> {
  paramName?: string;
  defaultTab: T;
  validTabs: readonly T[];
  scrollTargetRef?: RefObject<HTMLElement | null>;
  hashTrigger?: string;
}

interface UseTabSearchParamReturn<T extends string> {
  activeTab: T;
  setActiveTab: (tab: T) => void;
}

export function useTabSearchParam<T extends string>({
  paramName = "tab",
  defaultTab,
  validTabs,
  scrollTargetRef,
  hashTrigger,
}: UseTabSearchParamOptions<T>): UseTabSearchParamReturn<T> {
  const isInitialMount = useRef(true);
  const validTabsSet = useRef(new Set(validTabs));

  const [rawTab, setRawTab] = useQueryState(paramName);

  const isValidTab = (value: string | null): value is T => {
    return value !== null && validTabsSet.current.has(value as T);
  };

  const activeTab = isValidTab(rawTab) ? rawTab : defaultTab;

  // Handle hash trigger (e.g., #examples in URL)
  useEffect(() => {
    if (!hashTrigger || typeof window === "undefined") return;

    const hash = window.location.hash;
    if (hash === hashTrigger) {
      const hashTab = hashTrigger.replace("#", "") as T;
      if (validTabsSet.current.has(hashTab) && rawTab !== hashTab) {
        setRawTab(hashTab);
      }
    }
  }, [hashTrigger, rawTab, setRawTab]);

  // Handle scroll to target when switching to hash-triggered tab
  useEffect(() => {
    if (
      scrollTargetRef?.current &&
      hashTrigger &&
      activeTab === hashTrigger.replace("#", "") &&
      !isInitialMount.current
    ) {
      scrollTargetRef.current.scrollIntoView({
        behavior: "smooth",
        block: "start",
      });
    }
    isInitialMount.current = false;
  }, [activeTab, hashTrigger, scrollTargetRef]);

  const setActiveTab = useCallback(
    (newTab: T) => {
      if (newTab === activeTab) return;
      setRawTab(newTab);
    },
    [activeTab, setRawTab],
  );

  return { activeTab, setActiveTab };
}


--- lib/playground/prototypes/waymo/README.md ---
# Waymo Prototype (v0)

A reference implementation of Tool UIs for ride booking that follows the [Collaboration Guidelines](../../../../COLLAB_GUIDELINES.md).

## Quick Start

Navigate to `/playground/waymo-demo` to test the golden path:
1. Click "Start Demo: 'I need a ride home'"
2. See ride quote card
3. Click "Confirm Ride"
4. See booking confirmation

## Architecture

This prototype strictly follows the three-layer architecture:

### 1. Tools (`tools.ts`)
4 pure functions that return typed mock data:
- `getRiderContext()` - Returns user's home, work, payment methods
- `getPickupLocation({ hint })` - Returns pickup location with confidence
- `getQuote({ pickup, dropoff })` - Returns ride quote with price/ETA
- `bookTrip({ quoteId, paymentMethodId? })` - Books ride and returns confirmation

### 2. Tool UIs (`components/`)
2 React components that only render:
- `RideQuote` - Interactive (confirm button) → Receipt (confirmed state)
- `BookingConfirmation` - Receipt only (trip details + actions)

### 3. Orchestrator (`WaymoDemo.tsx`)
Manages the entire flow:
- Calls tools in sequence
- Updates message state
- Decides when to show Tool UIs
- Handles user interactions

## File Structure

```
waymo/
├── types.ts                    # Shared domain types
├── tools.ts                    # 4 core tools (mock data)
├── components/
│   ├── RideQuote.tsx          # Interactive → Receipt UI
│   ├── BookingConfirmation.tsx # Receipt UI
│   └── index.tsx              # Exports
├── WaymoDemo.tsx              # Orchestrator
├── README.md                  # This file
└── index.tsx                  # Main export
```

## Tool UI Message Format

```typescript
interface ToolUIMessage {
  type: "tool-ui";
  component: "RideQuote" | "BookingConfirmation";
  props: RideQuoteProps | BookingConfirmationProps;
}
```

## Golden Path Flow

1. **User**: "I need a ride home"
2. **Silent tools**: `getRiderContext` → `getPickupLocation` → `getQuote`
3. **Show**: Text + RideQuote UI (interactive)
4. **User clicks**: "Confirm Ride"
5. **Call**: `bookTrip`
6. **Update**: RideQuote → receipt, add BookingConfirmation

## Guidelines Compliance

This prototype demonstrates:
- ✅ **3-layer architecture** - Tools, UI components, orchestrator
- ✅ **4 tools only** - Chunky capabilities, not conversational steps
- ✅ **2 Tool UIs** - Minimal set for golden path
- ✅ **Standard message format** - Consistent ToolUIMessage shape
- ✅ **No tool calls in components** - All async logic in orchestrator
- ✅ **Typed domain objects** - Shared types in `types.ts`

## Extending

To add a friction variant (after golden path works):
1. Keep the same 4 tools
2. Reuse existing Tool UIs
3. Add branch logic in `WaymoDemo.tsx`
4. Test that golden path still works

## Important

**Do not:**
- Add more tools (stay at 4)
- Call tools from components
- Change ToolUIMessage format
- Break the golden path

**Always refer to [COLLAB_GUIDELINES.md](../../../../COLLAB_GUIDELINES.md) before making changes.**

## Links discovered
- [Collaboration Guidelines](https://github.com/assistant-ui/tool-ui/blob/main/COLLAB_GUIDELINES.md)
- [COLLAB_GUIDELINES.md](https://github.com/assistant-ui/tool-ui/blob/main/COLLAB_GUIDELINES.md)

--- lib/playground/prototypes/waymo/wip-tool-uis/README.md ---
# WIP Tool UIs

This directory contains **Work-In-Progress Tool UI components** for the Waymo Booking Assistant prototype. These are experimental UI components being developed and tested to determine:

1. Which tools benefit from custom UI components
2. What interaction patterns work best for different use cases
3. Which Tool UIs should be promoted to the main Tool UI library

## Purpose

The playground serves as a sandbox for:
- Prototyping Tool UIs for specific AI assistant use cases
- Experimenting with different interaction patterns
- Validating design decisions before committing to the main library
- Testing tool calling behavior and user flows

## Current Components

### FrequentLocationSelector

A shadcn/ui-based component that displays a user's frequent locations (favorites and recents) as an interactive picker.

**Tool:** `select_frequent_location`

**Use Case:** When a user requests a ride without specifying a destination, this UI presents their most frequently used locations (Home, Work, etc.) for quick selection.

**Features:**
- Categorizes locations into Favorites and Recents
- Uses contextual icons (Home, Work, generic location)
- Provides a fallback option to search for different locations
- Built entirely with shadcn/ui components for consistency

## Promotion Criteria

Tool UIs in this directory may be promoted to the main Tool UI library when they:
- Demonstrate clear value across multiple use cases
- Have stable APIs and interaction patterns
- Pass usability testing
- Show generic applicability beyond just Waymo

## Structure

```
wip-tool-uis/
├── README.md                      # This file
├── index.tsx                      # Exports all WIP components
└── FrequentLocationSelector.tsx  # Location picker component
```

## Development Guidelines

1. Use shadcn/ui components exclusively
2. Follow the existing project's TypeScript and styling conventions
3. Document the tool and use case clearly
4. Keep components focused and composable
5. Consider accessibility from the start


--- lib/presets/audio.ts ---
import type { SerializableAudio } from "@/components/tool-ui/audio";
import type { SerializableAction } from "@/components/tool-ui/shared";
import type { PresetWithCodeGen } from "./types";

interface AudioData {
  audio: SerializableAudio;
  responseActions?: SerializableAction[];
}

function escape(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function formatObject(value: Record<string, unknown>): string {
  return JSON.stringify(value, null, 2).replace(/\n/g, "\n  ");
}

function generateAudioCode(data: AudioData): string {
  const { audio, responseActions } = data;
  const props: string[] = [];

  props.push(`  id="${audio.id}"`);
  props.push(`  assetId="${audio.assetId}"`);
  props.push(`  src="${audio.src}"`);

  if (audio.title) {
    props.push(`  title="${escape(audio.title)}"`);
  }

  if (audio.description) {
    props.push(`  description="${escape(audio.description)}"`);
  }

  if (audio.artwork) {
    props.push(`  artwork="${audio.artwork}"`);
  }

  if (audio.durationMs) {
    props.push(`  durationMs={${audio.durationMs}}`);
  }

  if (audio.fileSizeBytes) {
    props.push(`  fileSizeBytes={${audio.fileSizeBytes}}`);
  }

  if (audio.createdAt) {
    props.push(`  createdAt="${audio.createdAt}"`);
  }

  if (audio.source) {
    props.push(`  source={${formatObject(audio.source as Record<string, unknown>)}}`);
  }

  if (responseActions && responseActions.length > 0) {
    props.push(
      `  responseActions={${JSON.stringify(responseActions, null, 4).replace(/\n/g, "\n  ")}}`,
    );
    props.push(
      `  onResponseAction={(actionId) => console.log("Action:", actionId)}`,
    );
  }

  return `<Audio\n${props.join("\n")}\n/>`;
}

export type AudioPresetName = "basic" | "with-artwork" | "with-actions";

export const audioPresets: Record<AudioPresetName, PresetWithCodeGen<AudioData>> = {
  basic: {
    description: "Simple audio player",
    data: {
      audio: {
        id: "audio-preview-basic",
        assetId: "audio-basic",
        src: "https://samplelib.com/lib/preview/mp3/sample-6s.mp3",
        title: "Sample audio clip",
      },
    } satisfies AudioData,
    generateExampleCode: generateAudioCode,
  },
  "with-artwork": {
    description: "Audio with artwork thumbnail and metadata",
    data: {
      audio: {
        id: "audio-preview-artwork",
        assetId: "audio-artwork",
        src: "https://samplelib.com/lib/preview/mp3/sample-6s.mp3",
        title: "Bell Labs hallway recording",
        description: "Ambient sounds where UNIX, C, and more took shape.",
        artwork: "https://images.unsplash.com/photo-1454165205744-3b78555e5572?w=400&auto=format&fit=crop",
        fileSizeBytes: 215040,
        durationMs: 30000,
        createdAt: "2025-02-12T10:15:00.000Z",
        source: {
          label: "Archive reel",
          iconUrl: "https://api.dicebear.com/7.x/shapes/svg?seed=reel",
        },
      },
    } satisfies AudioData,
    generateExampleCode: generateAudioCode,
  },
  "with-actions": {
    description: "Audio with response action buttons",
    data: {
      audio: {
        id: "audio-preview-actions",
        assetId: "audio-actions",
        src: "https://samplelib.com/lib/preview/mp3/sample-6s.mp3",
        title: "Podcast episode",
        description: "A deep dive into software architecture.",
        artwork: "https://images.unsplash.com/photo-1454165205744-3b78555e5572?w=400&auto=format&fit=crop",
        durationMs: 1800000,
      },
      responseActions: [
        { id: "share", label: "Share", variant: "default" },
        { id: "download", label: "Download", variant: "secondary" },
      ],
    } satisfies AudioData,
    generateExampleCode: generateAudioCode,
  },
};


--- lib/presets/chart.ts ---
import type { SerializableChart } from "@/components/tool-ui/chart";
import type { PresetWithCodeGen } from "./types";

type ChartData = Omit<SerializableChart, "id">;

export type ChartPresetName = "revenue" | "performance" | "minimal";

function generateChartCode(data: ChartData): string {
  const props: string[] = [];

  props.push(`  id="chart-example"`);
  props.push(`  type="${data.type}"`);

  if (data.title) {
    props.push(`  title="${data.title}"`);
  }

  if (data.description) {
    props.push(`  description="${data.description}"`);
  }

  props.push(
    `  data={${JSON.stringify(data.data, null, 4).replace(/\n/g, "\n  ")}}`,
  );
  props.push(`  xKey="${data.xKey}"`);
  props.push(
    `  series={${JSON.stringify(data.series, null, 4).replace(/\n/g, "\n  ")}}`,
  );

  if (data.colors) {
    props.push(
      `  colors={${JSON.stringify(data.colors, null, 4).replace(/\n/g, "\n  ")}}`,
    );
  }

  if (data.showLegend) {
    props.push(`  showLegend`);
  }

  if (data.showGrid) {
    props.push(`  showGrid`);
  }

  return `<Chart\n${props.join("\n")}\n/>`;
}

export const chartPresets: Record<ChartPresetName, PresetWithCodeGen<ChartData>> = {
  revenue: {
    description: "Bar chart with revenue vs expenses",
    data: {
      type: "bar",
      title: "Monthly Revenue",
      description: "Revenue vs Expenses (2024)",
      data: [
        { month: "Jan", revenue: 4000, expenses: 2400 },
        { month: "Feb", revenue: 3000, expenses: 1398 },
        { month: "Mar", revenue: 5000, expenses: 3200 },
        { month: "Apr", revenue: 2780, expenses: 3908 },
        { month: "May", revenue: 1890, expenses: 4800 },
        { month: "Jun", revenue: 2390, expenses: 3800 },
      ],
      xKey: "month",
      series: [
        { key: "revenue", label: "Revenue" },
        { key: "expenses", label: "Expenses" },
      ],
      showLegend: true,
      showGrid: true,
    } satisfies ChartData,
    generateExampleCode: generateChartCode,
  },
  performance: {
    description: "Line chart with system metrics",
    data: {
      type: "line",
      title: "System Performance",
      description: "CPU and Memory usage over time",
      data: [
        { time: "00:00", cpu: 45, memory: 62 },
        { time: "04:00", cpu: 32, memory: 58 },
        { time: "08:00", cpu: 67, memory: 71 },
        { time: "12:00", cpu: 89, memory: 85 },
        { time: "16:00", cpu: 76, memory: 79 },
        { time: "20:00", cpu: 54, memory: 68 },
      ],
      xKey: "time",
      series: [
        { key: "cpu", label: "CPU %" },
        { key: "memory", label: "Memory %" },
      ],
      showLegend: true,
      showGrid: true,
    } satisfies ChartData,
    generateExampleCode: generateChartCode,
  },
  minimal: {
    description: "Simple bar chart without title or legend",
    data: {
      type: "bar",
      data: [
        { category: "A", value: 100 },
        { category: "B", value: 200 },
        { category: "C", value: 150 },
        { category: "D", value: 300 },
      ],
      xKey: "category",
      series: [{ key: "value", label: "Value" }],
    } satisfies ChartData,
    generateExampleCode: generateChartCode,
  },
};


--- lib/mocks/chat-showcase-data.ts ---
import type { Column } from "@/components/tool-ui/data-table";
import type { SerializableLinkPreview } from "@/components/tool-ui/link-preview";
import type { SerializableChart } from "@/components/tool-ui/chart";
import type { XPostData } from "@/components/tool-ui/x-post";
import type { OptionListOption } from "@/components/tool-ui/option-list";
import type { SerializableTerminal } from "@/components/tool-ui/terminal";
import type { SerializableCodeBlock } from "@/components/tool-ui/code-block";
import type { SerializableItemCarousel } from "@/components/tool-ui/item-carousel";

export type Flight = {
  id: string;
  airline: string;
  route: string;
  departure: string;
  duration: string;
  stops: "Nonstop" | "1 stop" | "2 stops";
  price: string;
};

type BadgeColor = "danger" | "warning" | "info" | "success" | "neutral";

export const TABLE_COLUMNS: Column<Flight>[] = [
  { key: "airline", label: "Airline", sortable: true, priority: "primary" },
  { key: "route", label: "Route", sortable: false, priority: "primary" },
  { key: "departure", label: "Departs", sortable: true, priority: "secondary" },
  { key: "duration", label: "Duration", sortable: true, priority: "secondary" },
  {
    key: "stops",
    label: "Stops",
    sortable: true,
    priority: "primary",
    format: {
      kind: "badge",
      colorMap: {
        Nonstop: "success",
        "1 stop": "warning",
        "2 stops": "neutral",
      } as Record<string, BadgeColor>,
    },
  },
  { key: "price", label: "Price", sortable: true, priority: "primary" },
];

export const TABLE_DATA: Flight[] = [
  {
    id: "fl-1",
    airline: "ANA",
    route: "LAX → NRT",
    departure: "Mar 15, 11:30am",
    duration: "11h 45m",
    stops: "Nonstop",
    price: "$892",
  },
  {
    id: "fl-2",
    airline: "JAL",
    route: "LAX → HND",
    departure: "Mar 15, 1:15pm",
    duration: "12h 10m",
    stops: "Nonstop",
    price: "$945",
  },
  {
    id: "fl-3",
    airline: "United",
    route: "LAX → NRT",
    departure: "Mar 15, 10:45am",
    duration: "14h 20m",
    stops: "1 stop",
    price: "$724",
  },
  {
    id: "fl-4",
    airline: "Delta",
    route: "LAX → HND",
    departure: "Mar 15, 9:00am",
    duration: "16h 55m",
    stops: "1 stop",
    price: "$689",
  },
];

export const LINK_PREVIEW: SerializableLinkPreview = {
  id: "chat-showcase-link-preview",
  href: "https://www.quantamagazine.org/the-year-in-physics-20251217/",
  title: "The Year in Physics",
  description:
    "Physicists spotted a new black hole, doubled down on weakening dark energy, and debated the meaning of quantum mechanics.",
  image:
    "https://www.quantamagazine.org/wp-content/uploads/2025/12/Year-in-review-2025-Physics-cr-Carlos-Arrojo-Lede-1-1720x968.webp",
  domain: "quantamagazine.org",
  ratio: "16:9",
  createdAt: "2025-12-17T10:00:00.000Z",
};

export const X_POST: XPostData = {
  id: "chat-showcase-x-post",
  author: {
    name: "Noodle Enthusiast",
    handle: "ramenlover",
    avatarUrl:
      "https://images.unsplash.com/photo-1599566150163-29194dcaad36?auto=format&fit=crop&q=80&w=1200",
  },
  text: "Finally tried Tsujita LA and wow 🍜\n\nThe tsukemen broth is impossibly rich—thick, porky, with this subtle citrus note. Noodles have the perfect chew. Worth the 45-minute wait.\n\nOrdering extra chashu next time. This is the one.",
  createdAt: "2025-11-10T14:30:00.000Z",
};

export const X_POST_ACTIONS = [
  { id: "cancel", label: "Discard", variant: "ghost" as const },
  { id: "edit", label: "Revise", variant: "outline" as const },
  { id: "send", label: "Post Now", variant: "default" as const },
];

const SPENDING_DATA = [
  { category: "Groceries", amount: 284 },
  { category: "Dining", amount: 156 },
  { category: "Transport", amount: 89 },
  { category: "Entertainment", amount: 67 },
  { category: "Shopping", amount: 124 },
];

export const SPENDING_CHART: Omit<SerializableChart, "id"> = {
  type: "bar",
  title: "Weekly Spending",
  data: SPENDING_DATA,
  xKey: "category",
  series: [{ key: "amount", label: "Amount" }],
  showLegend: false,
};

export const PLAN_TODO_LABELS = [
  "Checking Sarah's interests",
  "Searching gift ideas",
  "Comparing top options",
  "Finalizing recommendations",
];

export const TERMINAL_DATA: Omit<SerializableTerminal, "id"> = {
  command: "pnpm test auth",
  stdout: `✓ login flow handles invalid credentials
✓ session tokens refresh correctly
✓ logout clears all cookies

Tests: 3 passed, 3 total
Time: 1.24s`,
  exitCode: 0,
  durationMs: 1243,
};

export const CODE_BLOCK_DATA: Omit<SerializableCodeBlock, "id"> = {
  language: "typescript",
  filename: "use-debounce.ts",
  code: `import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay = 250): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    // Normalize delay (covers NaN/Infinity/negative)
    const d = Number.isFinite(delay) ? Math.max(0, delay) : 0;

    // If no delay, update immediately.
    if (d === 0) {
      if (!Object.is(debounced, value)) setDebounced(value);
      return;
    }

    const id = setTimeout(() => {
      if (!Object.is(debounced, value)) setDebounced(value);
    }, d);

    return () => clearTimeout(id);
  }, [value, delay, debounced]);

  return debounced;
}`,
  showLineNumbers: true,
};

export const OPTION_LIST_OPTIONS: OptionListOption[] = [
  { id: "comedy", label: "Something funny", description: "I need a good laugh" },
  { id: "thriller", label: "Edge-of-seat thriller", description: "Keep me guessing" },
  { id: "comfort", label: "Feel-good classic", description: "Cozy and familiar" },
  { id: "scifi", label: "Mind-bending sci-fi", description: "Make me think" },
];

export const OPTION_LIST_CONFIRMED = ["comedy", "comfort"];

export const ITEM_CAROUSEL_DATA: Omit<SerializableItemCarousel, "id"> = {
  items: [
    {
      id: "ambient-1",
      name: "Music for Airports",
      subtitle: "Brian Eno · Ambient",
      image:
        "https://is1-ssl.mzstatic.com/image/thumb/Music125/v4/ee/71/42/ee71425d-6bc9-3df8-c90b-8539f59144ab/00724386649553.rgb.jpg/600x600bb.jpg",
      actions: [{ id: "play", label: "Play", variant: "default" }],
    },
    {
      id: "rock-1",
      name: "In Rainbows",
      subtitle: "Radiohead · Alt Rock",
      image:
        "https://is1-ssl.mzstatic.com/image/thumb/Music126/v4/dd/50/c7/dd50c790-99ac-d3d0-5ab8-e3891fb8fd52/634904032463.png/600x600bb.jpg",
      actions: [{ id: "play", label: "Play", variant: "default" }],
    },
    {
      id: "electronic-1",
      name: "Async",
      subtitle: "Ryuichi Sakamoto · Electronic",
      image:
        "https://is1-ssl.mzstatic.com/image/thumb/Music123/v4/82/e0/7b/82e07b9a-1d98-bbf4-d1e2-fb94312bbea2/731383683060.jpg/600x600bb.jpg",
      actions: [{ id: "play", label: "Play", variant: "default" }],
    },
    {
      id: "jazz-1",
      name: "Kind of Blue",
      subtitle: "Miles Davis · Jazz",
      image:
        "https://is1-ssl.mzstatic.com/image/thumb/Music/7f/9f/d6/mzi.vtnaewef.jpg/600x600bb.jpg",
      actions: [{ id: "play", label: "Play", variant: "default" }],
    },
    {
      id: "psychedelic-1",
      name: "Currents",
      subtitle: "Tame Impala · Psychedelic",
      image:
        "https://is1-ssl.mzstatic.com/image/thumb/Music115/v4/a8/2e/b4/a82eb490-f30a-a321-461a-0383c88fec95/15UMGIM23316.rgb.jpg/600x600bb.jpg",
      actions: [{ id: "play", label: "Play", variant: "default" }],
    },
  ],
};


--- lib/ui/cn.ts ---
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}


--- lib/presets/code-block.ts ---
import type { SerializableCodeBlock } from "@/components/tool-ui/code-block";
import type { PresetWithCodeGen } from "./types";

export type CodeBlockPresetName =
  | "typescript"
  | "python"
  | "json"
  | "bash"
  | "highlighted"
  | "collapsible";

function escape(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`");
}

function generateCodeBlockCode(data: SerializableCodeBlock): string {
  const props: string[] = [];

  props.push(`  code={\`${escape(data.code)}\`}`);
  props.push(`  language="${data.language}"`);

  if (data.filename) {
    props.push(`  filename="${data.filename}"`);
  }

  if (data.showLineNumbers !== undefined) {
    props.push(`  showLineNumbers={${data.showLineNumbers}}`);
  }

  if (data.highlightLines && data.highlightLines.length > 0) {
    props.push(`  highlightLines={[${data.highlightLines.join(", ")}]}`);
  }

  if (data.maxCollapsedLines) {
    props.push(`  maxCollapsedLines={${data.maxCollapsedLines}}`);
  }

  return `<CodeBlock\n${props.join("\n")}\n/>`;
}

export const codeBlockPresets: Record<
  CodeBlockPresetName,
  PresetWithCodeGen<SerializableCodeBlock>
> = {
  typescript: {
    description: "TypeScript with filename header",
    data: {
      id: "code-block-preview-typescript",
      code: `import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}`,
      language: "typescript",
      filename: "Counter.tsx",
      showLineNumbers: true,
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
  python: {
    description: "Python function with docstring",
    data: {
      id: "code-block-preview-python",
      code: `def fibonacci(n: int) -> list[int]:
    """Generate Fibonacci sequence up to n terms."""
    if n <= 0:
        return []
    elif n == 1:
        return [0]

    sequence = [0, 1]
    while len(sequence) < n:
        sequence.append(sequence[-1] + sequence[-2])

    return sequence

# Example usage
print(fibonacci(10))`,
      language: "python",
      filename: "fibonacci.py",
      showLineNumbers: true,
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
  json: {
    description: "JSON configuration file",
    data: {
      id: "code-block-preview-json",
      code: `{
  "name": "tool-ui",
  "version": "1.0.0",
  "dependencies": {
    "react": "^19.0.0",
    "zod": "^4.0.0",
    "shiki": "^3.0.0"
  }
}`,
      language: "json",
      filename: "package.json",
      showLineNumbers: true,
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
  bash: {
    description: "Bash script with comments",
    data: {
      id: "code-block-preview-bash",
      code: `#!/bin/bash
# Deploy script

echo "Building application..."
pnpm run build

echo "Running tests..."
pnpm test

echo "Deploying to production..."
rsync -avz ./dist/ user@server:/var/www/app/

echo "Done!"`,
      language: "bash",
      filename: "deploy.sh",
      showLineNumbers: true,
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
  highlighted: {
    description: "Code with highlighted lines (bug indicator)",
    data: {
      id: "code-block-preview-highlighted",
      code: `function processData(items: string[]) {
  const results = [];

  for (const item of items) {
    // BUG: This should handle null values
    results.push(item.toUpperCase());
  }

  return results;
}`,
      language: "typescript",
      filename: "processor.ts",
      showLineNumbers: true,
      highlightLines: [5, 6],
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
  collapsible: {
    description: "Long code with collapse/expand",
    data: {
      id: "code-block-preview-collapsible",
      code: Array.from(
        { length: 30 },
        (_, i) => `console.log("Line ${i + 1}");`,
      ).join("\n"),
      language: "javascript",
      showLineNumbers: true,
      maxCollapsedLines: 10,
    } satisfies SerializableCodeBlock,
    generateExampleCode: generateCodeBlockCode,
  },
};


--- lib/playground/constants.ts ---
export const PROTOTYPE_SLUG_HEADER = "x-prototype-slug";


--- lib/presets/data-table.ts ---
import type { Column, RowPrimitive } from "@/components/tool-ui/data-table";
import type { SerializableAction } from "@/components/tool-ui/shared";
import type { PresetWithCodeGen } from "./types";

type GenericRow = Record<string, RowPrimitive>;

export type SortState = { by?: string; direction?: "asc" | "desc" };

export interface DataTableData {
  id: string;
  columns: Column[];
  data: GenericRow[];
  rowIdKey?: string;
  defaultSort?: SortState;
  maxHeight?: string;
  emptyMessage?: string;
  locale?: string;
  responseActions?: SerializableAction[];
}

function generateDataTableCode(data: DataTableData): string {
  const props: string[] = [];

  props.push(
    `  columns={${JSON.stringify(data.columns, null, 4).replace(/\n/g, "\n  ")}}`,
  );

  props.push(
    `  data={${JSON.stringify(data.data, null, 4).replace(/\n/g, "\n  ")}}`,
  );

  if (data.rowIdKey) {
    props.push(`  rowIdKey="${data.rowIdKey}"`);
  }

  if (data.defaultSort) {
    props.push(
      `  defaultSort={${JSON.stringify(data.defaultSort, null, 4).replace(/\n/g, "\n  ")}}`,
    );
  }

  if (data.emptyMessage && data.emptyMessage !== "No data available") {
    props.push(`  emptyMessage="${data.emptyMessage}"`);
  }

  if (data.maxHeight) {
    props.push(`  maxHeight="${data.maxHeight}"`);
  }

  if (data.locale) {
    props.push(`  locale="${data.locale}"`);
  }

  if (data.responseActions && data.responseActions.length > 0) {
    props.push(
      `  responseActions={${JSON.stringify(data.responseActions, null, 4).replace(/\n/g, "\n  ")}}`,
    );
    props.push(
      `  onResponseAction={(actionId) => console.log("Action:", actionId)}`,
    );
  }

  return `<DataTable\n${props.join("\n")}\n/>`;
}

const stockColumns: Column<GenericRow>[] = [
  { key: "symbol", label: "Symbol", priority: "primary" },
  { key: "name", label: "Company", priority: "primary" },
  { key: "price", label: "Price", align: "right", priority: "primary", format: { kind: "currency", currency: "USD", decimals: 2 } },
  { key: "change", label: "Change", align: "right", priority: "secondary", format: { kind: "delta", decimals: 2, upIsPositive: true, showSign: true } },
  { key: "changePercent", label: "Change %", align: "right", priority: "secondary", format: { kind: "percent", decimals: 2, showSign: true, basis: "unit" } },
  { key: "volume", label: "Volume", align: "right", priority: "secondary", format: { kind: "number", compact: true } },
];

const stockData: GenericRow[] = [
  { symbol: "IBM", name: "International Business Machines", price: 170.42, change: 1.12, changePercent: 0.66, volume: 18420000 },
  { symbol: "AAPL", name: "Apple", price: 178.25, change: 2.35, changePercent: 1.34, volume: 52430000 },
  { symbol: "MSFT", name: "Microsoft", price: 380.0, change: 1.24, changePercent: 0.33, volume: 31250000 },
  { symbol: "INTC", name: "Intel Corporation", price: 39.85, change: -0.42, changePercent: -1.04, volume: 29840000 },
  { symbol: "ORCL", name: "Oracle Corporation", price: 110.31, change: 0.78, changePercent: 0.71, volume: 14230000 },
];

const taskColumns: Column<GenericRow>[] = [
  { key: "title", label: "Task", priority: "primary" },
  { key: "status", label: "Status", priority: "primary", format: { kind: "status", statusMap: { todo: { tone: "neutral", label: "Todo" }, in_progress: { tone: "info", label: "In Progress" }, done: { tone: "success", label: "Done" }, blocked: { tone: "danger", label: "Blocked" } } } },
  { key: "priority", label: "Priority", priority: "secondary", format: { kind: "status", statusMap: { low: { tone: "success" }, medium: { tone: "warning" }, high: { tone: "danger" }, critical: { tone: "danger", label: "Critical" } } } },
  { key: "assignee", label: "Assignee", priority: "secondary" },
  { key: "dueDate", label: "Due Date", priority: "secondary", format: { kind: "date", dateFormat: "short" } },
  { key: "completedDate", label: "Completed", priority: "tertiary", format: { kind: "date", dateFormat: "long" } },
  { key: "isUrgent", label: "Urgent", priority: "tertiary", format: { kind: "boolean" } },
];

const taskData: GenericRow[] = [
  { title: "Transcribe punch cards to magnetic tape", status: "in_progress", priority: "high", assignee: "Grace", dueDate: "2025-11-05", completedDate: null, isUrgent: true },
  { title: "Port FORTRAN IV routines to C", status: "todo", priority: "critical", assignee: "Dennis", dueDate: "2025-11-04", completedDate: null, isUrgent: true },
  { title: "Document UNIX pipeline patterns", status: "done", priority: "low", assignee: "Ken", dueDate: "2025-10-28", completedDate: "2025-10-27", isUrgent: false },
  { title: "Design WIMP interface prototype", status: "blocked", priority: "medium", assignee: "Adele", dueDate: "2025-11-10", completedDate: null, isUrgent: false },
  { title: "Optimize RISC instruction scheduling", status: "in_progress", priority: "medium", assignee: "Sophie", dueDate: "2025-11-08", completedDate: null, isUrgent: false },
];

const resourceColumns: Column<GenericRow>[] = [
  { key: "name", label: "Resource", priority: "primary" },
  { key: "category", label: "Category", priority: "primary", format: { kind: "badge", colorMap: { documentation: "info", tutorial: "success", reference: "neutral", tool: "warning" } } },
  { key: "url", label: "Link", priority: "secondary", format: { kind: "link", external: true } },
  { key: "localPath", label: "Local Copy", priority: "secondary", format: { kind: "link" } },
  { key: "tags", label: "Tags", priority: "secondary", format: { kind: "array", maxVisible: 2 } },
  { key: "updatedAt", label: "Last Updated", priority: "tertiary", format: { kind: "date", dateFormat: "relative" } },
];

const resourceData: GenericRow[] = [
  { name: "The ENIAC Story", category: "Documentation", url: "https://www.computerhistory.org/revolution/early-computers/5", localPath: "/docs/eniac-story.pdf", tags: ["eniac", "vacuum-tube", "history"], updatedAt: "2025-05-12T09:00:00.000Z" },
  { name: "The UNIX Philosophy", category: "Reference", url: "https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch01s06.html", localPath: "/docs/unix-philosophy.md", tags: ["unix", "pipe", "philosophy"], updatedAt: "2025-05-12T13:00:00.000Z" },
  { name: "ARPANET Origins", category: "Tutorial", url: "https://www.internetsociety.org/internet/history-internet/brief-history-internet/", localPath: null, tags: ["arpanet", "internet", "packet-switching"], updatedAt: "2025-05-12T18:00:00.000Z" },
  { name: "Xerox PARC Research", category: "Tool", url: "https://xeroxparc.archive.org/", localPath: "/docs/parc-archive/", tags: ["gui", "wimp", "innovation"], updatedAt: "2025-05-06T09:00:00.000Z" },
];

const actionsColumns: Column<GenericRow>[] = [
  { key: "id", label: "Ticket", priority: "primary" },
  { key: "subject", label: "Subject", priority: "primary", truncate: true },
  { key: "priority", label: "Priority", priority: "primary", format: { kind: "status", statusMap: { urgent: { tone: "danger", label: "Urgent" }, high: { tone: "warning", label: "High" }, normal: { tone: "neutral", label: "Normal" } } } },
  { key: "status", label: "Status", priority: "secondary", format: { kind: "status", statusMap: { open: { tone: "info", label: "Open" }, pending: { tone: "warning", label: "Pending" }, escalated: { tone: "danger", label: "Escalated" } } } },
  { key: "waitTime", label: "Wait Time", abbr: "Wait", align: "right", priority: "secondary", format: { kind: "delta", decimals: 0, upIsPositive: false, showSign: false } },
  { key: "createdAt", label: "Created", priority: "tertiary", format: { kind: "date", dateFormat: "relative" } },
];

const actionsData: GenericRow[] = [
  { id: "TKT-4521", subject: "Payment failed - urgent customer escalation", priority: "urgent", status: "escalated", waitTime: 36, createdAt: "2025-11-23T08:15:00.000Z" },
  { id: "TKT-4518", subject: "Cannot access account after password reset", priority: "high", status: "open", waitTime: 12, createdAt: "2025-11-24T14:30:00.000Z" },
  { id: "TKT-4515", subject: "Billing discrepancy on November invoice", priority: "high", status: "pending", waitTime: 8, createdAt: "2025-11-24T18:45:00.000Z" },
  { id: "TKT-4512", subject: "Feature request: export to PDF", priority: "normal", status: "open", waitTime: 4, createdAt: "2025-11-25T09:00:00.000Z" },
];

export type DataTablePresetName = "stocks" | "tasks" | "resources" | "actions";

export const dataTablePresets: Record<DataTablePresetName, PresetWithCodeGen<DataTableData>> = {
  stocks: {
    description: "Market data with currency, delta, and percent formatting",
    data: {
      id: "data-table-preview-stocks",
      columns: stockColumns,
      data: stockData,
      rowIdKey: "symbol",
    },
    generateExampleCode: generateDataTableCode,
  },
  tasks: {
    description: "Status pills, boolean badges, and multiple date formats",
    data: {
      id: "data-table-preview-tasks",
      columns: taskColumns,
      data: taskData,
      rowIdKey: "title",
    },
    generateExampleCode: generateDataTableCode,
  },
  resources: {
    description: "External and internal links, tag arrays, badges, and relative dates",
    data: {
      id: "data-table-preview-resources",
      columns: resourceColumns,
      data: resourceData,
      rowIdKey: "name",
    },
    generateExampleCode: generateDataTableCode,
  },
  actions: {
    description: "Support queue with response actions and wait time indicators",
    data: {
      id: "data-table-preview-actions",
      columns: actionsColumns,
      data: actionsData,
      rowIdKey: "id",
      defaultSort: { by: "waitTime", direction: "desc" },
      responseActions: [
        { id: "close", label: "Close tickets", confirmLabel: "Confirm close", variant: "destructive" },
        { id: "escalate", label: "Escalate", variant: "secondary" },
        { id: "assign", label: "Assign to me", variant: "default" },
      ],
    },
    generateExampleCode: generateDataTableCode,
  },
};


--- lib/presets/image.ts ---
import type { SerializableImage } from "@/components/tool-ui/image";
import type { SerializableAction } from "@/components/tool-ui/shared";
import type { PresetWithCodeGen } from "./types";

interface ImageData {
  image: SerializableImage;
  responseActions?: SerializableAction[];
}

function escape(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function formatObject(value: Record<string, unknown>): string {
  return JSON.stringify(value, null, 2).replace(/\n/g, "\n  ");
}

function generateImageCode(data: ImageData): string {
  const { image, responseActions } = data;
  const props: string[] = [];

  props.push(`  id="${image.id}"`);
  props.push(`  assetId="${image.assetId}"`);
  props.push(`  src="${image.src}"`);
  props.push(`  alt="${escape(image.alt)}"`);

  if (image.title) {
    props.push(`  title="${escape(image.title)}"`);
  }

  if (image.description) {
    props.push(`  description="${escape(image.description)}"`);
  }

  if (image.href) {
    props.push(`  href="${image.href}"`);
  }

  if (image.domain) {
    props.push(`  domain="${image.domain}"`);
  }

  if (image.ratio) {
    props.push(`  ratio="${image.ratio}"`);
  }

  if (image.fit) {
    props.push(`  fit="${image.fit}"`);
  }

  if (image.fileSizeBytes) {
    props.push(`  fileSizeBytes={${image.fileSizeBytes}}`);
  }

  if (image.createdAt) {
    props.push(`  createdAt="${image.createdAt}"`);
  }

  if (image.source) {
    props.push(`  source={${formatObject(image.source as Record<string, unknown>)}}`);
  }

  if (responseActions && responseActions.length > 0) {
    props.push(
      `  responseActions={${JSON.stringify(responseActions, null, 4).replace(/\n/g, "\n  ")}}`,
    );
    props.push(
      `  onResponseAction={(actionId) => console.log("Action:", actionId)}`,
    );
  }

  return `<Image\n${props.join("\n")}\n/>`;
}

export type ImagePresetName = "basic" | "with-source" | "with-actions";

export const imagePresets: Record<ImagePresetName, PresetWithCodeGen<ImageData>> = {
  basic: {
    description: "Simple image with title and description",
    data: {
      image: {
        id: "image-preview-basic",
        assetId: "image-basic",
        src: "https://images.unsplash.com/photo-1504548840739-580b10ae7715?w=1200&auto=format&fit=crop",
        alt: "Vintage mainframe with blinking lights",
        title: "From mainframes to microchips",
        description: "A snapshot of when rooms were computers.",
        ratio: "4:3",
        domain: "unsplash.com",
      },
    } satisfies ImageData,
    generateExampleCode: generateImageCode,
  },
  "with-source": {
    description: "Image with source attribution and metadata",
    data: {
      image: {
        id: "image-preview-source",
        assetId: "image-source",
        src: "https://images.unsplash.com/photo-1504548840739-580b10ae7715?w=1200&auto=format&fit=crop",
        alt: "Vintage mainframe with blinking lights",
        title: "From mainframes to microchips",
        description: "A snapshot of when rooms were computers — not just what ran inside them.",
        ratio: "4:3",
        domain: "unsplash.com",
        createdAt: "2025-02-10T15:30:00.000Z",
        fileSizeBytes: 2457600,
        source: {
          label: "Computing archives",
          iconUrl: "https://api.dicebear.com/7.x/shapes/svg?seed=archives",
          url: "https://assistant-ui.com/tools/alignment",
        },
      },
    } satisfies ImageData,
    generateExampleCode: generateImageCode,
  },
  "with-actions": {
    description: "Image with response action buttons",
    data: {
      image: {
        id: "image-preview-actions",
        assetId: "image-actions",
        src: "https://images.unsplash.com/photo-1518770660439-4636190af475?w=1200&auto=format&fit=crop",
        alt: "Circuit board with processor chip",
        title: "System architecture diagram",
        description: "A detailed overview of the microprocessor layout.",
        ratio: "16:9",
        domain: "unsplash.com",
        createdAt: "2025-03-15T10:00:00.000Z",
      },
      responseActions: [
        { id: "download", label: "Download", variant: "secondary" },
        { id: "share", label: "Share", variant: "default" },
      ],
    } satisfies ImageData,
    generateExampleCode: generateImageCode,
  },
};
