USP
Tambo provides a fullstack solution, handling LLM conversation loops, streaming infrastructure, and conversation state management, so developers can focus on defining components with Zod schemas. It uniquely allows agents to directly "spea…
Use cases
- 01Building AI chat interfaces with dynamic component generation
- 02Creating AI-powered analytics dashboards with visualizations
- 03Integrating generative UI into existing React applications
- 04Developing agents that render and update UI components
- 05Adding dynamic, interactive elements controlled by an AI agent
Detected files (8)
.claude/skills/api-resource-lifecycle/SKILL.mdskillShow content (7849 bytes)
--- name: api-resource-lifecycle description: Guides CRUD operations for API resources with cascading dependencies, descriptive validation, and orphan prevention. Use when adding delete/remove operations, creating validation logic, building resources that depend on other resources, or when the user mentions "cascade delete", "orphan records", "duplicate detection", "validation errors", "resource cleanup", or "rollback on failure". metadata: internal: true --- # API Resource Lifecycle Patterns for building reliable CRUD operations in Tambo Cloud. ## Gotchas - The codebase uses `CONFLICT` for duplicate name errors in skills but `BAD_REQUEST` for the same pattern in MCP servers. Always use `CONFLICT` for duplicates. - PostgreSQL unique violation code is `23505`. Catch it and map to a domain-specific exception rather than letting the raw DB error propagate. - When replacing a provider key, all skills with `externalSkillMetadata` for that provider must have their metadata cleared, or they'll reference a stale key. - Skills are silently skipped (not errored) at runtime when the provider doesn't support them. This is intentional -- don't add validation that blocks skill creation for unsupported providers. - `Promise.allSettled` (not `Promise.all`) for batch external API calls -- partial failures need cleanup, not an all-or-nothing abort. ## Cascading Deletes Default to `onDelete: "cascade"` in the schema. Only use manual transaction cascades when deletion requires external API calls, metadata cleanup, or cross-reference logic. ```typescript // packages/db/src/schema.ts export const skills = pgTable("skills", { id: text("id").primaryKey(), projectId: text("project_id") .notNull() .references(() => projects.id, { onDelete: "cascade" }), // ... }); ``` ### Manual cascades in transactions When deletion requires cleanup beyond FK cascades (external APIs, metadata, cross-references), wrap in a transaction: ```typescript // packages/db/src/operations/project.ts export async function deleteProject(db: HydraDb, id: string): Promise<boolean> { return await db.transaction(async (tx) => { await tx .delete(schema.providerKeys) .where(eq(schema.providerKeys.projectId, id)); await tx.delete(schema.apiKeys).where(eq(schema.apiKeys.projectId, id)); await tx .delete(schema.projectMembers) .where(eq(schema.projectMembers.projectId, id)); const deleted = await tx .delete(schema.projects) .where(eq(schema.projects.id, id)) .returning(); return deleted.length > 0; }); } ``` **Reference:** `packages/db/src/operations/project.ts` lines 255-278 ### Metadata cleanup on replacement When a resource is replaced (not deleted), clear dependent metadata so dependents re-sync under the new resource: ```typescript // apps/web/server/api/routers/project.ts - addProviderKey // When replacing a provider key, clear skill metadata for that provider const skills = await operations.listSkillsForProject(ctx.db, projectId); await Promise.all( skills .filter((s) => s.externalSkillMetadata?.[providerName]) .map(async (s) => { const { [providerName]: _, ...remaining } = s.externalSkillMetadata ?? {}; return operations.updateSkill(ctx.db, { projectId, skillId: s.id, externalSkillMetadata: remaining, }); }), ); ``` **Reference:** `apps/web/server/api/routers/project.ts` lines 863-880 ### Rules - If a child record has no meaning without its parent, use `onDelete: "cascade"` in the schema - If deletion requires multi-step cleanup or external API calls, wrap in a transaction - When replacing a resource, clear dependent metadata so dependents re-sync - Always delete children before the parent in manual cascades - Check for affected dependents and document the cascade chain in code comments ## Validation Errors Every error must say what went wrong and what to do instead. Include the specific value that failed and an example of what's valid. ```typescript // Zod: include format example name: z.string().min(1, "Name is required").max(64) .regex(SKILL_NAME_PATTERN, "Name must be kebab-case (e.g. scheduling-assistant)"), // tRPC: include the conflicting value throw new TRPCError({ code: "CONFLICT", message: `Server key "${serverKey}" is already in use by another MCP server in this project`, }); ``` ### Error code mapping | Situation | tRPC Code | When to use | | ----------------------- | ----------------------- | ------------------------------------- | | Resource not found | `NOT_FOUND` | ID lookup returned null | | Input validation failed | `BAD_REQUEST` | Zod schema or business rule violation | | Duplicate resource | `CONFLICT` | Name/key already exists | | Unexpected failure | `INTERNAL_SERVER_ERROR` | Catch-all for unhandled errors | ## Duplicate Detection Default to database unique constraints with custom exception mapping. Only use pre-creation queries when no DB constraint exists. ```typescript // packages/db/src/operations/skills.ts const PG_UNIQUE_VIOLATION = "23505"; const SKILLS_NAME_UNIQUE_CONSTRAINT = "skills_project_id_name_idx"; export class SkillNameConflictError extends Error { constructor(name: string) { super(`A skill named "${name}" already exists in this project`); this.name = "SkillNameConflictError"; } } export async function createSkill( db: HydraDb, data: NewSkill, ): Promise<DBSkill> { try { const [skill] = await db.insert(schema.skills).values(data).returning(); return skill; } catch (error) { if (isSkillNameConflict(error)) { throw new SkillNameConflictError(data.name); } throw error; } } ``` ### Pre-creation queries (fallback) When no unique DB constraint exists, query before inserting: ```typescript const existingKeys = await getExistingServerKeys(ctx.db, projectId); if (existingKeys.includes(serverKey)) { throw new TRPCError({ code: "CONFLICT", message: `Server key "${serverKey}" is already in use by another MCP server in this project`, }); } ``` **Reference:** `apps/web/server/api/routers/tools.ts` lines 119-126 ## Orphan Prevention When creating a resource that syncs to an external API, roll back on failure: ```typescript // apps/web/server/api/routers/skills.ts // Create in DB first, then sync to provider const skill = await operations.createSkill(ctx.db, { ... }); // If provider sync fails, delete the DB record try { await createSkillOnProviderAndPersist(ctx.db, input.projectId, skill); } catch (error) { await operations.deleteSkill(ctx.db, input.projectId, skill.id); throw error; } ``` **Rules:** - Create the DB record first (gives you an ID for the external API call) - If the external sync fails, delete the DB record to avoid orphans - The user sees an error and can retry, rather than seeing a broken record - For batch operations, use `Promise.allSettled` and clean up partial failures **Reference:** `apps/web/server/api/routers/skills.ts` lines 195-204 ## Checklist When building a new CRUD operation: ``` Resource Lifecycle Checklist: - [ ] Delete cascades documented (which dependents are affected?) - [ ] Database-level cascades set up for simple parent-child FK relationships - [ ] Manual cascade logic wrapped in a transaction for complex cleanup - [ ] All Zod validation messages are descriptive with examples - [ ] TRPCError messages include the specific value that caused the error - [ ] Duplicate detection before creation (custom exception or pre-query) - [ ] Error codes are correct (CONFLICT for duplicates, NOT_FOUND for missing) - [ ] External API sync failures roll back the DB record - [ ] Resource replacement clears dependent metadata ```.claude/skills/building-settings-ui/SKILL.mdskillShow content (10082 bytes)
--- name: building-settings-ui description: >- Use this skill when adding or modifying settings UI in Tambo Cloud. Covers where a new settings section belongs (Agent tab vs Settings tab), and the component patterns used across both pages (card layout, toasts, confirmation dialogs, destructive styling, save behavior conventions). Triggers on "add a new settings section", "where should X go?", "settings UI", "settings page", "agent page", or any work touching apps/web/components/dashboard-components/project-details/, project-settings.tsx, or agent-settings.tsx. Not for full-stack feature building (DB, tRPC, tests); those patterns will get their own skills. metadata: internal: true --- # Building Settings UI Guide for placing and styling settings sections in the Tambo Cloud dashboard. Covers two concerns: where a feature belongs (which tab/page), and how to build the UI component to match existing patterns. ## Architecture Settings are split across two top-level tabs in the project layout: - **Agent tab** (`/[projectId]/agent`) - How the AI agent behaves - **Settings tab** (`/[projectId]/settings`) - Project infrastructure and access Each tab renders a flat vertical stack of Card components. There is no sidebar navigation; each page is short enough to scroll naturally. ### Tab layout ``` Overview | Observability | Agent | Settings ``` - Layout file: `apps/web/app/(authed)/(dashboard)/[projectId]/layout.tsx` - Agent page: `apps/web/app/(authed)/(dashboard)/[projectId]/agent/page.tsx` - Settings page: `apps/web/app/(authed)/(dashboard)/[projectId]/settings/page.tsx` ## Gotchas 1. **Do not add a new top-level tab** without explicit team alignment. Current tabs (Overview, Observability, Agent, Settings) have been stable. 2. **`EditWithTamboButton` goes inside `CardTitle`**, not as a sibling of `CardHeader`. It must have a `description` prop explaining what the section configures. 3. **Invalidate the query before toasting** in `onSuccess`. Reversing the order can show a success toast while the UI still displays old data. 4. **Use `DeleteConfirmationDialog`**, never inline `AlertDialog` for destructive confirmations. 5. **Use `text-destructive` semantic color**, never `text-red-500`. Cancel/discard buttons are NOT destructive. --- ## Feature Placement ### Agent Tab Sections | # | Section | What it configures | Component | | --- | ------------------- | -------------------------------------------------- | -------------------------------- | | 1 | Model | Provider + model selection, API key, custom params | `provider-key-section.tsx` | | 2 | Custom Instructions | System prompt, prompt override toggle | `custom-instructions-editor.tsx` | | 3 | Skills | Skill definitions and imports | `skills-section.tsx` | | 4 | Tool Call Limit | Max tool calls per response | `tool-call-limit-editor.tsx` | | 5 | MCP | MCP server URLs + headers | `available-mcp-servers.tsx` | **Container:** `apps/web/components/dashboard-components/agent-settings.tsx` ### Settings Tab Sections | # | Section | What it configures | Component | | --- | -------------- | ------------------------------ | -------------------------- | | 1 | Name | Project display name | `project-name-section.tsx` | | 2 | API Keys | API key list + create | `api-key-list.tsx` | | 3 | Authentication | OAuth mode, token requirements | `oauth-settings.tsx` | | 4 | Danger Zone | Project deletion | `danger-zone-section.tsx` | **Container:** `apps/web/components/dashboard-components/project-settings.tsx` All section components live in `apps/web/components/dashboard-components/project-details/`. ### Placement Decision Tree 1. **Configures AI agent behavior?** (model selection, prompts, tools, memory, context) -> **Agent tab** 2. **Configures project infrastructure?** (API keys, naming, deletion, billing, webhooks) -> **Settings tab** 3. **Configures who can access?** (auth, tokens, team members, permissions) -> **Settings tab** 4. **Monitoring or debugging view?** -> **Observability tab** 5. **High-level summary or status?** -> **Overview tab** 6. **None of the above?** -> Ask the user. ### Conditional and Dependent Settings Some settings only apply when another setting is in a specific state. Follow these patterns: **Show but warn (soft dependency).** The section renders normally but displays an `Alert` when the dependency isn't met. The user can still see and configure the setting. Use this when the feature exists but won't work at runtime. Example: Skills section shows a provider compatibility notice when the selected provider doesn't support skills: ```tsx // skills-section.tsx const isProviderSupported = SKILLS_SUPPORTED_PROVIDERS.has( defaultLlmProviderName, ); // Renders full skills UI + warning Alert if !isProviderSupported ``` **Conditionally pass props (data dependency).** The parent reads one setting and passes it as a prop so the child can adapt its behavior. Use this when the child's content or options change based on the parent's state. Example: MCP servers section receives `providerType` to toggle agent-mode-specific UI: ```tsx // agent-settings.tsx <AvailableMcpServers providerType={projectData?.providerType} />; // available-mcp-servers.tsx const isAgentMode = providerType === AiProviderType.AGENT; ``` Example: Custom LLM parameters change available suggestions based on provider and model: ```tsx // provider-key-section.tsx passes selectedProvider to the parameter editor <CustomLlmParametersEditor selectedProvider={selectedProvider} /> ``` **Rules for new dependent settings:** 1. Never hide a section entirely based on another setting's state. Always render the card; use an Alert or disabled state to communicate the dependency. 2. The warning message must name the dependency and tell the user what to change (e.g., "Switch to a supported provider to enable skills"). 3. Keep dependency checks in the section component, not the container. The container (`agent-settings.tsx`, `project-settings.tsx`) should pass data, not make visibility decisions. 4. If a setting depends on state from a different tab, pass it through the shared project query (`api.project.getProject`) rather than cross-tab state. ### Adding a New Section 1. **Create the component** in `project-details/` following the Card layout pattern below. 2. **Import and render** in the correct container (`agent-settings.tsx` or `project-settings.tsx`). 3. **Update the skeleton** in `settings-skeletons.tsx` (either `AgentPageSkeleton` or `SettingsPageSkeleton`). --- ## Component Patterns ### Card Layout Every settings section uses `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent` from `@/components/ui/card`. ```tsx <Card> <CardHeader> <CardTitle className="text-lg font-semibold"> Section Name <EditWithTamboButton description="Configure section settings..." /> </CardTitle> <CardDescription className="text-sm font-sans text-foreground"> Description of what this section does. </CardDescription> </CardHeader> <CardContent className="space-y-4">{/* Content */}</CardContent> </Card> ``` ### Toast Notifications Every mutation shows a toast on both success and error. Import `useToast` from `@/hooks/use-toast`. ```tsx const mutation = api.someRoute.someMutation.useMutation({ onSuccess: async () => { await utils.someRoute.someQuery.invalidate(); toast({ title: "Success", description: "Setting updated successfully" }); }, onError: () => { toast({ title: "Error", description: "Failed to update setting", variant: "destructive", }); }, }); ``` Never skip the error toast. ### Confirmation Dialogs All destructive actions use `DeleteConfirmationDialog` from `@/components/dashboard-components/delete-confirmation-dialog`: ```tsx const [alertState, setAlertState] = useState<{ show: boolean; title: string; description: string; data?: { id: string }; }>({ show: false, title: "", description: "" }); <DeleteConfirmationDialog mode="single" alertState={alertState} setAlertState={setAlertState} onConfirm={handleConfirmDelete} />; ``` Title includes the item name (`Delete "${name}"?`). Description warns the action cannot be undone. ### Destructive Action Styling ```tsx <Button variant="ghost" size="icon" className="text-destructive hover:text-destructive hover:bg-destructive/10" > <Trash2 className="h-4 w-4" /> </Button> ``` Use `Trash2` from `lucide-react`. Use `hover:bg-destructive/10` for ghost variant hover. ### Save Behavior **Toggles: Immediate save.** `onCheckedChange` fires the mutation directly. Include `aria-label` with state context. **Form fields: Edit/Save/Cancel.** Track `isEditing`, `savedValue`, and `displayValue` state. Cancel reverts to `savedValue`. Save button disabled during mutation, shows "Saving...". `autoFocus` on first input when entering edit mode. **Reference implementations:** `custom-instructions-editor.tsx` (edit/save/cancel), `tool-call-limit-editor.tsx` (simpler form), `project-name-section.tsx` (basic edit/save/cancel). ### Danger Zone Pattern For irreversible destructive actions, use the Danger Zone card pattern: ```tsx <Card className="border-destructive/50"> <CardHeader> <CardTitle>Danger Zone</CardTitle> <CardDescription>Warning about permanence.</CardDescription> </CardHeader> <CardContent> <Button variant="ghost" className="text-destructive hover:text-destructive hover:bg-destructive/10" aria-label="Delete this project" > Delete this project </Button> </CardContent> </Card> ``` The `DeleteConfirmationDialog` should be owned by the parent component that handles the mutation and post-delete navigation..claude/skills/validating-accessibility/SKILL.mdskillShow content (5168 bytes)
--- name: validating-accessibility description: >- Use this skill when creating, modifying, or reviewing any .tsx component in apps/web, even if the user doesn't mention "accessibility." Covers semantic HTML, aria labels, navigation landmarks, forms, dialogs, and keyboard navigation. Trigger on: adding buttons, links, toggles, icons, or any interactive element; building or editing forms; adding dialogs or modals; reviewing UI code. Includes inline verification patterns for scanning violations. Not for styling or layout changes that don't involve interactive elements. metadata: internal: true --- # Accessibility Checklist Every UI component in `apps/web` must meet these standards. No partial compliance. ## Gotchas - **`role="button"` divs may exist in the codebase** -- fix them when touching affected files. `<TableHead>` elements with `role="button"` for sortable columns are acceptable. - **Nested interactive elements** -- when replacing a `<div role="button">` that contains a child `<button>` (e.g., a copy button inside a collapsible toggle), do not just swap the outer div to `<button>`. That creates invalid nested buttons. Instead, restructure into sibling elements: a toggle `<button>` and a separate action `<button>` side by side in a flex container. - **Standalone inputs outside react-hook-form need manual ID pairing** -- use `useId()` with `htmlFor`/`id`. The shadcn `<FormField>` handles this automatically, but raw `<Input>` does not. - **AlertDialog vs Dialog** -- use `AlertDialog` for destructive confirmations (requires `AlertDialogTitle` + `AlertDialogDescription`). Use `Dialog` for content/forms. Never build custom modal overlays. - **Icon-only buttons without `aria-label`** are common in new code. Every icon-only button needs one, and it must include context: `Delete API key ${keyName}`, not just "Delete". ## Semantic HTML Use native elements. Never recreate `<button>` behavior with `<div role="button">` + keyboard handlers. | Interaction | Element | | ---------------- | ------------------------------------------------------ | | Clickable action | `<button>` or `<Button>` from `@/components/ui/button` | | Navigation link | `<Link>` (Next.js) or `<a>` | | Navigation group | `<nav>` with descriptive `aria-label` | | Item list | `<ul>`/`<ol>` + `<li>` | | Section heading | `<h1>`-`<h6>` in order, never skip levels | ## Aria Labels Every interactive element without visible text needs `aria-label` with both action AND target: ```tsx <Button size="icon" aria-label={`Delete API key ${keyName}`}> <Trash2 className="h-4 w-4" /> </Button> <Switch aria-label={`${enabled ? "Disable" : "Enable"} skill ${skillName}`} /> ``` Prefer state-aware labels ("Copied!" vs "Copy"). Buttons with visible text skip `aria-label`. **Reference implementations:** `copy-button.tsx` (state-aware), `context-attachment-badge.tsx` (contextual remove), `thread-table-header.tsx` (sort state) -- all in `apps/web/components/`. ## Navigation Landmarks Wrap navigation groups in `<nav>` with a unique `aria-label` per region on the page. ## Forms Use shadcn Form components from `@/components/ui/form` (`FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormMessage`). They handle ID generation, label association, `aria-describedby`, and `aria-invalid` automatically. For standalone inputs outside react-hook-form, pair `useId()` with `htmlFor`/`id`. Never use placeholder as label substitute. ## Keyboard Navigation - Only `tabIndex={0}` or `tabIndex={-1}` (never positive values) - Never remove focus outlines - Prefer `<button>` over manual Enter/Space handlers ## Verification Scan `apps/web/components` for common violations. For each check, grep for the pattern and fix any matches found. ### Check 1: `role="button"` on non-button elements Search for `role="button"` in `.tsx` files. Flag `<div` or `<span` elements with this attribute; they should be `<button>` or `<Button>` instead. `<TableHead>` elements with `role="button"` for sortable columns are acceptable. **Pattern:** `role="button"` ### Check 2: `<div onClick>` patterns Search for `<div` elements with `onClick` handlers. These should use `<button>` instead for proper keyboard support. **Pattern:** `<div[^>]*onClick` ### Check 3: Positive tabIndex values Search for `tabIndex` with values greater than 0. Only `tabIndex={0}` and `tabIndex={-1}` are allowed. **Pattern:** `tabIndex={[1-9]` ### Check 4: Icon buttons missing aria-label Search for `size="icon"` in `.tsx` files. For each match, check surrounding lines (5-10 above and below) for `aria-label` on the same `<Button>` element or an `sr-only` span. Flag buttons that have neither. **Pattern:** `size="icon"` without nearby `aria-label` ### Manual checks These cannot be detected by pattern matching: - [ ] Form inputs have associated `<label>` elements - [ ] Navigation groups use `<nav>` with unique `aria-label` - [ ] Dialogs use Radix-based components (AlertDialog or Dialog) - [ ] Focus outlines intactplugins/tambo/skills/building-with-tambo/SKILL.mdskillShow content (16332 bytes)
--- name: building-with-tambo description: Integrates Tambo into existing React apps — detects tech stack, installs @tambo-ai/react, wires TamboProvider, registers components with Zod schemas, and sets up tools/context. Use when adding AI-powered generative UI to an existing codebase. Triggers on "add Tambo", "integrate Tambo", "add AI chat to my app", "add generative UI", or when the user has an existing React/Next.js/Vite project and wants to add AI-powered components. For brand-new projects, use generative-ui instead. --- # Building with Tambo Detect tech stack and integrate Tambo while preserving existing patterns. ## Reference Guides Load these when you reach the relevant step or need deeper implementation details: - [Components](references/components.md) - **Load at Step 5.** Generative vs interactable components, propsSchema, ComponentRenderer. - [Component Rendering](references/component-rendering.md) - Streaming props, loading states, persistent state. Load when building custom message rendering. - [Threads and Input](references/threads.md) - **Load when building custom chat UI.** useTambo(), useTamboThreadInput(), userKey/userToken auth, suggestions, voice. - [Tools and Context](references/tools-and-context.md) - **Load when wiring host app APIs.** defineTool(), MCP servers, contextHelpers. - [CLI Reference](references/cli.md) - **Load at Step 6.** `tambo add` component library, `tambo init` flags, non-interactive mode. - [Skills](references/skills.md) - **Mention as a next step after setup.** Project-scoped agent skills via CLI and dashboard. - [Add Components to Registry](references/add-components-to-registry.md) - **Load when registering existing app components.** Analyzes props, generates Zod schemas, writes descriptions. Shared references (components, rendering, threads, tools/context, CLI, skills) are duplicated into generative-ui so each skill works independently. `add-components-to-registry` is unique to this skill. ## Workflow 1. **Detect tech stack** - Analyze package.json, lock files, project structure, monorepo layout 2. **Confirm with user** - Present findings, ask about preferences 3. **Install dependencies** - Add @tambo-ai/react and zod using the project's package manager 4. **Create provider setup** - Wire TamboProvider with apiKey, userKey, components 5. **Create component registry** - Set up lib/tambo.ts 6. **Add chat UI** - Install pre-built Tambo components via CLI, set up path aliases and globals.css ## Step 1: Detect Tech Stack Check these files to understand the project: ```bash # Key files to read package.json # Dependencies, scripts, AND package manager tsconfig.json # TypeScript config, path aliases next.config.* # Next.js vite.config.* # Vite tailwind.config.* # Tailwind CSS postcss.config.* # PostCSS src/index.* or app/ # Entry points yarn.lock / pnpm-lock.yaml / package-lock.json # Which package manager ``` ### Detection Checklist | Technology | Detection | | ---------------- | ------------------------------------------------- | | Next.js | `next` in dependencies, `next.config.*` exists | | Vite | `vite` in devDependencies, `vite.config.*` exists | | Create React App | `react-scripts` in dependencies | | TypeScript | `typescript` in deps, `tsconfig.json` exists | | Tailwind | `tailwindcss` in deps, config file exists | | Plain CSS | No Tailwind, CSS files in src/ | | Zod | `zod` in dependencies | | Other validation | `yup`, `joi`, `superstruct` in deps | ### Package Manager Detection **Always detect and use the project's package manager.** Do not assume npm. | Lock file | Manager | Install command | | ------------------- | ------- | ----------------------------- | | `package-lock.json` | npm | `npm install @tambo-ai/react` | | `yarn.lock` | Yarn | `yarn add @tambo-ai/react` | | `pnpm-lock.yaml` | pnpm | `pnpm add @tambo-ai/react` | For **monorepos**, install in the correct workspace: - Yarn: `yarn workspace <app-name> add @tambo-ai/react` - pnpm: `pnpm --filter <app-name> add @tambo-ai/react` - npm: `npm install @tambo-ai/react -w <app-name>` ### Monorepo Detection Check for monorepo indicators: - `workspaces` field in root `package.json` - `pnpm-workspace.yaml` - `turbo.json` or `nx.json` - Multiple `package.json` files in `apps/` or `packages/` If monorepo detected, identify which package is the web app that will use Tambo (usually in `apps/web`, `apps/frontend`, or similar). ### Global Keyboard Shortcuts Detection Check if the app captures keyboard events globally (common in drawing tools, editors, IDEs). Look for: - `document.addEventListener("keydown", ...)` in the codebase - Canvas-based apps (Excalidraw, Figma-like, code editors) - Keyboard shortcut libraries (hotkeys-js, mousetrap, etc.) If found, the Tambo chat UI wrapper must stop event propagation (see Step 6). ## Step 2: Confirm with User Present findings including the package manager and any special concerns: ``` I detected your project uses: - Framework: Next.js 14 (App Router) - Package manager: Yarn - Styling: Tailwind CSS - Validation: No Zod (will need to add) - TypeScript: Yes - Monorepo: No - Global keyboard shortcuts: No Should I: 1. Install Tambo with these settings? 2. Use plain CSS instead of Tailwind for Tambo components? 3. Something else? ``` ## Step 3: Install Dependencies Use the project's package manager (detected in Step 1): ```bash # npm npm install @tambo-ai/react npm install zod # if no Zod installed # yarn yarn add @tambo-ai/react yarn add zod # pnpm pnpm add @tambo-ai/react pnpm add zod # Monorepo (install in the correct workspace) yarn workspace <app-name> add @tambo-ai/react zod pnpm --filter <app-name> add @tambo-ai/react zod npm install @tambo-ai/react zod -w <app-name> ``` ## Step 4: Create Provider Setup ### Next.js App Router ```tsx // app/providers.tsx "use client"; import { TamboProvider } from "@tambo-ai/react"; import { components } from "@/lib/tambo"; export function Providers({ children }: { children: React.ReactNode }) { return ( <TamboProvider apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY} userKey="default-user" components={components} > {children} </TamboProvider> ); } ``` **IMPORTANT:** `userKey` is required for authentication. Without it, message submission fails at runtime with "authentication is not ready." Note: `userKey` is typed as optional in TypeScript (`userKey?: string`), so the compiler won't catch this — the failure is purely at runtime. In production, use a real user identifier (e.g., session ID, user ID from your auth system). For development/demo, a static string such as `"default-user"` works. ```tsx // app/layout.tsx import { Providers } from "./providers"; export default function RootLayout({ children }) { return ( <html> <body> <Providers>{children}</Providers> </body> </html> ); } ``` ### Next.js Pages Router ```tsx // pages/_app.tsx import { TamboProvider } from "@tambo-ai/react"; import { components } from "@/lib/tambo"; export default function App({ Component, pageProps }) { return ( <TamboProvider apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY} userKey="default-user" components={components} > <Component {...pageProps} /> </TamboProvider> ); } ``` ### Vite / CRA ```tsx // src/main.tsx import { TamboProvider } from "@tambo-ai/react"; import { components } from "./lib/tambo"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render( <TamboProvider apiKey={import.meta.env.VITE_TAMBO_API_KEY} userKey="default-user" components={components} > <App /> </TamboProvider>, ); ``` ## Step 5: Create Component Registry ```tsx // lib/tambo.ts (or src/lib/tambo.ts) import { TamboComponent } from "@tambo-ai/react"; export const components: TamboComponent[] = [ // Components will be registered here ]; ``` ## Step 6: Add Chat UI Pick the right chat layout for the app: | Component | Best for | How it renders | | ---------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------- | | `message-thread-collapsible` | Overlaying on top of existing UI (drawing tools, simple apps) | Fixed-position floating panel, bottom-right corner, toggle open/close | | `message-thread-panel` | Apps with sidebar layouts (dashboards, admin panels, SaaS apps) | Side panel with thread history, resizable | | `message-thread-full` | Dedicated chat pages or full-screen chat experiences | Full-height thread with all features | Choose based on the app's layout. If there's a sidebar or dashboard layout, use `panel`. If the chat should float over existing content, use `collapsible`. If the whole page is the chat, use `full`. ```bash npx tambo add message-thread-panel --yes # Or: npx tambo add message-thread-collapsible --yes # Or: npx tambo add message-thread-full --yes # With other package managers: # yarn dlx tambo add <component> --yes # pnpm dlx tambo add <component> --yes ``` ### After `tambo add`, complete the setup: 1. **CSS setup** — `tambo add` adds Tailwind directives and CSS variables to your project's CSS entry file. The file it modifies depends on the framework: - **Next.js**: `app/globals.css` (or `src/app/globals.css`) — already imported in layout by default. - **Vite**: `src/index.css` (or `index.css`) — already imported in `main.tsx` by default. If your entry file already imports a CSS file, `tambo add` modifies that file in place. No new import is needed. 2. **Path alias** — Tambo components use `@/` imports. Check if the project's tsconfig already has `@/*` in its `paths`. Many Next.js projects created with `create-next-app` have this, but not all (e.g., Cal.com uses `~/*` and `@components/*` instead). If `@/*` is missing, add it to the app tsconfig (`tsconfig.app.json` when present, otherwise `tsconfig.json`): ```json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ``` If the project has no `src/` directory and components land in the project root, use `["./*"]` instead of `["./src/*"]`. Check where `tambo add` placed the components to determine the correct path. For **Vite projects**, also add the alias to `vite.config.ts`: ```ts // vite.config.ts import path from "path"; export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, }); ``` ### Keyboard Event Isolation **Critical for apps with global keyboard shortcuts** (drawing tools, editors, terminal-like apps). Without this, typing in the Tambo chat input triggers the host app's shortcuts. Wrap the Tambo chat UI in a div that stops keyboard event propagation: ```tsx <div onKeyDown={(e) => e.stopPropagation()} onKeyUp={(e) => e.stopPropagation()} > {/* Use whichever component you installed */} <MessageThreadCollapsible />{" "} {/* or <MessageThreadPanel /> or <MessageThreadFull /> */} </div> ``` ### Z-Index for Full-Screen Apps Apps with full-screen canvases or overlays may render on top of the Tambo chat. The `MessageThreadCollapsible` component uses `position: fixed`, but the host app's canvas may have a higher stacking context. Wrap with a fixed container that sits above everything: ```tsx <div style={{ position: "fixed", inset: 0, zIndex: 9999, pointerEvents: "none" }} > <div style={{ pointerEvents: "auto" }}> {/* Use whichever component you installed */} <MessageThreadCollapsible />{" "} {/* or <MessageThreadPanel /> or <MessageThreadFull /> */} </div> </div> ``` The outer div creates a stacking context above the canvas, while `pointerEvents: none/auto` ensures only the chat panel is clickable — the rest of the screen passes clicks through to the canvas. You can combine keyboard isolation and z-index in one wrapper. ## Adapting to Existing Patterns ### No Tailwind? Use Plain CSS If the project doesn't use Tailwind, `tambo add` will install Tailwind v4 via PostCSS alongside the existing styling. This is additive — it won't break existing CSS/SCSS. The Tambo components use Tailwind, but the rest of your app keeps its styling. If you'd prefer to avoid Tailwind entirely: ```bash # Skip --yes flag to customize styling during add npx tambo add message-thread-full # Select "CSS Modules" or "Plain CSS" when prompted ``` ### Existing Validation Library? If using Yup/Joi instead of Zod, user can either: 1. Add Zod just for Tambo schemas (recommended - small addition) 2. Convert schemas (more work, not recommended) ### Monorepo? Run commands from the web app package, not the monorepo root. Run `npx tambo init --project-name=<app-name>` from the web app directory. This opens the browser for authentication and polls until the user completes auth (up to 15 minutes). Use a long timeout. Once auth completes, the CLI creates the project and writes the API key to `.env.local`. **Monorepo gotchas:** - **Env vars**: `tambo init` creates `.env.local` in the current directory. Vite loads env from `envDir` (check `vite.config.*`). Ensure the env file is where Vite looks for it. - **Dependency hoisting**: Some package managers hoist deps to the monorepo root. If Vite can't resolve a Tambo dependency, install it at the root level too (e.g., `yarn add -W @tambo-ai/react`). - **Path aliases**: Vite resolves from the `vite.config.*` directory. The `@/` alias must resolve to the correct `src/` directory within the workspace, not the monorepo root. ## Environment Variables `npx tambo init --project-name=<name>` opens the browser and polls until the user authenticates (up to 15 minutes). Use a long timeout. Once auth completes, it creates the project and writes `.env.local`. If manual setup is needed (monorepo, read-only filesystem), add the appropriate variable: | Framework | Variable | | --------- | --------------------------- | | Next.js | `NEXT_PUBLIC_TAMBO_API_KEY` | | Vite | `VITE_TAMBO_API_KEY` | | CRA | `REACT_APP_TAMBO_API_KEY` | ## Verification After setup, verify by creating a simple test: ```tsx import { useTambo } from "@tambo-ai/react"; function TestComponent() { const { thread, isIdle } = useTambo(); console.log("Tambo connected:", isIdle); return <div>Tambo is set up!</div>; } ``` ## Reporting Bugs & Feedback If you encounter any of these while helping a user, suggest filing a GitHub issue: - A bug in `@tambo-ai/react`, `@tambo-ai/client`, or the `tambo` CLI - Documentation that contradicts actual behavior - Missing or misleading instructions in this skill ### Issue Template Use this template when drafting an issue for `tambo-ai/tambo`: ````markdown **Title:** [Short description of the problem] **Expected behavior** What should happen. **Actual behavior** What happens instead, including any error messages or stack traces. **Repro steps** 1. Run `npx tambo create-app ...` (or whatever command) 2. Add this code: ```tsx // minimal code that reproduces the issue ``` 3. Observe the error **Environment** - `@tambo-ai/react` version: - `@tambo-ai/client` version: - Framework (Next.js / Vite / CRA) and version: - Node.js version: - OS: **Additional context** Link to relevant docs or skill file path if applicable. ```` **Security:** Redact API keys, tokens, and any customer data before including logs or code snippets in the issue. ### Suggesting an Issue to the User When you hit a problem that looks like a Tambo bug, say something like: > This looks like a bug in `@tambo-ai/react`. Want me to open a GitHub issue on `tambo-ai/tambo` with the repro steps and environment details? Always wait for the user to confirm before filing..claude/skills/creating-styled-wrappers/SKILL.mdskillShow content (7752 bytes)
--- name: creating-styled-wrappers description: Creates styled wrapper components that compose headless/base compound components. Use when refactoring styled components to use base primitives, implementing opinionated design systems on top of headless components, or when the user mentions "use base components", "compose primitives", "styled wrapper", or "refactor to use base". metadata: internal: true --- # Styling Compound Wrappers Create styled wrapper components that compose headless base compound components. This skill complements `building-compound-components` (which builds the base primitives) by focusing on **how to properly consume and wrap them** with styling and additional behavior. **Real-world example**: See [references/real-world-example.md](references/real-world-example.md) for a complete before/after MessageInput refactoring. ## Core Principle: Compose, Don't Duplicate Styled wrappers should **compose** base components, not **re-implement** their logic. ```tsx // WRONG - re-implementing what base already does const StyledInput = ({ children, className }) => { const { value, setValue, submit } = useTamboThreadInput(); // Duplicated! const [isDragging, setIsDragging] = useState(false); // Duplicated! const handleDrop = useCallback(/* ... */); // Duplicated! return ( <form onDrop={handleDrop} className={className}> {children} </form> ); }; // CORRECT - compose the base component const StyledInput = ({ children, className, variant }) => { return ( <BaseInput.Root className={cn(inputVariants({ variant }), className)}> <BaseInput.Content className="rounded-xl data-[dragging]:border-dashed"> {children} </BaseInput.Content> </BaseInput.Root> ); }; ``` ## Refactoring Workflow Copy this checklist and track progress: ``` Styled Wrapper Refactoring: - [ ] Step 1: Identify duplicated logic - [ ] Step 2: Import base components - [ ] Step 3: Wrap with Base Root - [ ] Step 4: Apply state-based styling and behavior - [ ] Step 5: Wrap sub-components with styling - [ ] Step 6: Final verification ``` ### Step 1: Identify Duplicated Logic Look for patterns that indicate logic should come from base: - SDK hooks (`useTamboThread`, `useTamboThreadInput`, etc.) - Context creation (`React.createContext`) - State management that mirrors base component state - Event handlers (drag, submit, etc.) that base components handle ### Step 2: Import Base Components ```tsx import { MessageInput as MessageInputBase } from "@tambo-ai/react-ui-base/message-input"; ``` ### Step 3: Wrap with Base Root Replace custom context/state management with the base Root: ```tsx // Before const MessageInput = ({ children, variant }) => { return ( <MessageInputInternal variant={variant}>{children}</MessageInputInternal> ); }; // After const MessageInput = ({ children, variant, className }) => { return ( <MessageInputBase.Root className={cn(variants({ variant }), className)}> {children} </MessageInputBase.Root> ); }; ``` ### Step 4: Apply State-Based Styling and Behavior State access follows a hierarchy — use the simplest option that works: 1. **Data attributes** (preferred for styling) — base components expose `data-*` attributes 2. **Render props** (for behavior changes) — use when rendering different components 3. **Context hooks** (for sub-components) — OK for styled sub-components needing deep context access ```tsx // BEST - data-* classes for styling, render props only for behavior // Note: use `data-[dragging]:*` syntax (v3-compatible), not `data-dragging:*` (v4 only) const StyledContent = ({ children }) => ( <BaseComponent.Content className={cn( "group rounded-xl border", "data-[dragging]:border-dashed data-[dragging]:border-emerald-400", )} > {({ elicitation, resolveElicitation }) => ( <> {/* Drop overlay uses group-data-* for styling */} <div className="hidden group-data-[dragging]:flex absolute inset-0 bg-emerald-50/90"> <p>Drop files here</p> </div> {elicitation ? ( <ElicitationUI request={elicitation} onResponse={resolveElicitation} /> ) : ( children )} </> )} </BaseComponent.Content> ); // OK - styled sub-components can use context hook for deep access const StyledTextarea = ({ placeholder }) => { const { value, setValue, handleSubmit, editorRef } = useMessageInputContext(); return ( <CustomEditor ref={editorRef} value={value} onChange={setValue} onSubmit={handleSubmit} placeholder={placeholder} /> ); }; ``` **When to use context hooks vs render props:** - Render props: when the parent wrapper needs state for behavior changes - Context hooks: when a styled sub-component needs values not exposed via render props ### Step 5: Wrap Sub-Components ```tsx // Submit button const SubmitButton = ({ className, children }) => ( <BaseComponent.SubmitButton className={cn("w-10 h-10 rounded-lg", className)}> {({ showCancelButton }) => children ?? (showCancelButton ? <Square /> : <ArrowUp />) } </BaseComponent.SubmitButton> ); // Error const Error = ({ className }) => ( <BaseComponent.Error className={cn("text-sm text-destructive", className)} /> ); // Staged images - base pre-computes props array, just iterate const StagedImages = ({ className }) => ( <BaseComponent.StagedImages className={cn("flex gap-2", className)}> {({ images }) => images.map((imageProps) => ( <ImageBadge key={imageProps.image.id} {...imageProps} /> )) } </BaseComponent.StagedImages> ); ``` ### Step 6: Final Verification ``` Final Checks: - [ ] No duplicate context creation - [ ] No duplicate SDK hooks in root wrappers - [ ] No duplicate state management or event handlers - [ ] Base namespace imported and `Base.Root` used as wrapper - [ ] `data-*` classes used for styling (with `group-data-*` for children) - [ ] Render props used only for rendering behavior changes - [ ] Base sub-components wrapped with styling - [ ] Icon factories passed from styled layer to base hooks - [ ] Visual sub-components and CSS variants stay in styled layer ``` ## What Belongs in Styled Layer ### Icon Factories When base hooks need icons, pass a factory function: ```tsx // Base hook accepts optional icon factory export function useCombinedResourceList( providers: ResourceProvider[] | undefined, search: string, createMcpIcon?: (serverName: string) => React.ReactNode, ) { /* ... */ } // Styled layer provides the factory const resources = useCombinedResourceList(providers, search, (serverName) => ( <McpServerIcon name={serverName} className="w-4 h-4" /> )); ``` ### CSS Variants ```tsx const inputVariants = cva("w-full", { variants: { variant: { default: "", solid: "[&>div]:shadow-xl [&>div]:ring-1", bordered: "[&>div]:border-2", }, }, }); ``` ### Layout Logic, Visual Sub-Components, Custom Data Fetching These all stay in the styled layer. Base handles behavior; styled handles presentation. ## Type Handling Handle ref type differences between base and styled components: ```tsx // Base context may have RefObject<T | null> // Styled component may need RefObject<T> <TextEditor ref={editorRef as React.RefObject<TamboEditor>} /> ``` ## Anti-Patterns - **Re-implementing base logic** - if base handles it, compose it - **Using render props for styling** - prefer `data-*` classes; render props are for behavior changes - **Duplicating context in wrapper** - use base Root which provides context - **Hardcoding icons in base hooks** - use factory functions to keep styling in styled layer.claude/skills/ai-sdk-model-manager/SKILL.mdskillShow content (7767 bytes)
--- name: ai-sdk-model-manager description: Manages AI SDK model configurations - updates packages, identifies missing models, adds new models with research, and updates documentation metadata: internal: true --- # AI SDK Model Manager This skill helps maintain AI SDK model configurations in the Tambo Cloud codebase. It automates the process of keeping model definitions up-to-date with the latest AI SDK releases. ## What This Skill Does 1. **Updates AI SDK Packages** - Checks and updates @ai-sdk/openai, @ai-sdk/google, @ai-sdk/groq, and other provider packages to their latest versions 2. **Identifies Missing Models** - Compares TypeScript definitions in the SDKs against configured models to find newly available models 3. **Researches Models** - Gathers information about new models including capabilities, context windows, pricing, and use cases 4. **Prompts User** - Asks which models to add before making changes 5. **Adds Models** - Updates model configuration files with proper TypeScript types and metadata 6. **Updates Documentation** - Updates relevant docs and README files to reflect new model availability ## When to Use This Skill Use this skill when: - You want to check if AI SDK packages need updating - New models have been released by OpenAI, Google, Anthropic, or other providers - You're getting TypeScript errors about model IDs not being in SDK types - You want to ensure Tambo supports the latest models ## Files This Skill Works With - `packages/core/src/llms/models/*.ts` - Model configuration files - `packages/backend/package.json` - AI SDK dependencies (source of truth for versions) - `docs/content/docs/models/*.mdx` - Model documentation - `README.md` - Main documentation file ## Process ### Step 1: Update AI SDK Packages Check current versions and update to latest: ```bash cd packages/backend npm outdated | grep '@ai-sdk' npm install @ai-sdk/openai@latest @ai-sdk/google@latest @ai-sdk/groq@latest @ai-sdk/anthropic@latest @ai-sdk/mistral@latest ``` ### Step 2: Identify Missing Models For each provider, inspect the TypeScript definitions: ```bash # Check what models are in the SDK types cat node_modules/@ai-sdk/openai/dist/index.d.ts | grep 'type.*ModelId' cat node_modules/@ai-sdk/google/dist/index.d.ts | grep 'type.*ModelId' cat node_modules/@ai-sdk/groq/dist/index.d.ts | grep 'type.*ModelId' ``` Compare against current model configurations in: - `packages/core/src/llms/models/openai.ts` - `packages/core/src/llms/models/gemini.ts` - `packages/core/src/llms/models/groq.ts` - `packages/core/src/llms/models/anthropic.ts` - `packages/core/src/llms/models/mistral.ts` ### Step 3: Research New Models **Use the researcher subagent to gather information about each missing model:** ``` Launch a researcher subagent to find: - Official documentation link - Model capabilities (reasoning, vision, function calling, etc.) - Context window size (inputTokenLimit) - Pricing tier - Best use cases - Release date and status (experimental, stable, deprecated) ``` The researcher subagent has access to web search and can efficiently gather this information for multiple models in parallel. ### Step 4: Prompt User Present findings: ``` Found the following new models in updated AI SDK packages: OpenAI: - gpt-6-preview (200k context, experimental reasoning model) - gpt-4.2-turbo (1M context, improved function calling) Google: - gemini-3.5-pro (2M context, advanced reasoning) Which models would you like to add? (all/none/specific) ``` Wait for user response before proceeding. ### Step 5: Add Selected Models **Consider launching parallel subagents to add models to each provider file:** For models spread across multiple providers (OpenAI, Google, Groq), launch separate subagents to edit each file concurrently. This is faster than doing them sequentially. For each model being added, ensure these required fields: - `apiName`: Exact model ID string from SDK - `displayName`: Human-friendly name - `status`: "untested" | "tested" | "known-issues" - `notes`: Brief description of capabilities and use cases - `docLink`: Official provider documentation URL - `tamboDocLink`: "https://docs.tambo.co" - `inputTokenLimit`: Context window size in tokens - `modelSpecificParams`: Any special parameters (reasoning, thinking, etc.) Follow existing patterns in each file and ensure model IDs match SDK type definitions exactly. Example: ```typescript "gpt-6-preview": { apiName: "gpt-6-preview", displayName: "gpt-6-preview", status: "untested", notes: "Experimental next-generation reasoning model with extended context", docLink: "https://platform.openai.com/docs/models/gpt-6-preview", tamboDocLink: "https://docs.tambo.co", inputTokenLimit: 200000, modelSpecificParams: reasoningParameters, }, ``` ### Step 6: Verify TypeScript Run type checking to ensure all model IDs are valid: ```bash cd packages/core npm run check-types ``` If there are type errors, fix model IDs to match SDK definitions exactly. ### Step 7: Update Documentation **Consider using subagents to update documentation in parallel:** If updating multiple documentation files, launch parallel subagents to handle: 1. **README.md** - Update the "Supported LLM Providers" section if new providers or significant models were added 2. **docs/content/docs/models/\*.mdx** - Add new models to appropriate documentation pages with: - Model name and description - Key capabilities - Context window - Example use cases - Links to provider docs ### Step 8: Run Quality Checks Before completing: ```bash cd packages/core npm run lint npm run check-types npm run test ``` ### Step 9: Create Pull Request **Create a PR with proper conventional commit format:** ```bash gh pr create --title "feat(models): add [model names] support" --body "$(cat <<'EOF' Updated AI SDK packages and added support for newly released models: Models added: - [list models here] Package updates: - @ai-sdk/openai: X.X.X → X.X.X - @ai-sdk/groq: X.X.X → X.X.X All type checks passing, documentation updated. EOF )" ``` **PR title format:** `feat(models): add [model names] support` Use `feat(models):` for new models or `deps(core):` for package updates only. ## Guidelines - **Use subagents for efficiency** - Launch researcher subagents for gathering information and parallel subagents for editing multiple files - **Always research before adding** - Don't guess at model capabilities or context limits - **Match SDK types exactly** - Model IDs must match the TypeScript definitions in node_modules - **Mark new models as "untested"** - Let the team test before marking as "tested" - **Include official doc links** - Always link to provider's official documentation - **Be conservative** - Only add models the user explicitly approves - **Update docs comprehensively** - Don't just update code, update all relevant documentation ## Error Handling If you encounter: - **Type errors after adding models** - Double-check the model ID matches the SDK's TypeScript definition exactly - **Missing model in SDK** - The provider may not have released it yet, suggest waiting for next SDK update - **Conflicting model names** - Use the SDK's preferred naming convention - **Unknown context limits** - Research provider docs or mark as "unknown" and note it needs verification ## Notes - This skill should be run periodically (monthly or when new models are announced) - Always check the git diff before committing to ensure only intended changes were made - Some models may have special requirements (API access, pricing tier, etc.) - note these in the model's `notes` field - If a model is renamed in the SDK, update both the key and apiName, and consider adding a deprecation note to the old entry.claude/skills/compound-components/SKILL.mdskillShow content (8331 bytes)
--- name: building-compound-components description: Creates unstyled compound components that separate business logic from styles. Use when building headless UI primitives, creating component libraries, implementing Radix-style namespaced components, or when the user mentions "compound components", "headless", "unstyled", "primitives", or "render props". metadata: internal: true --- # Building Compound Components Create unstyled, composable React components following the Radix UI / Base UI pattern. Components expose behavior via context while consumers control rendering. ## Project Rules These rules are specific to this codebase and override general patterns. ### Hooks Are Internal Hooks are implementation details, not public API. **Never export hooks from the index.** ```tsx // index.tsx - CORRECT export const Component = { Root: ComponentRoot, Content: ComponentContent, }; export type { ComponentRootProps, ComponentContentRenderProps }; // index.tsx - WRONG export { useComponentContext }; // Don't export hooks ``` Consumers access state via **render props**, not hooks. When styled wrappers in the **same package** need hook access, import directly from the source file: ```tsx import { useComponentContext } from "../base/component/component-context"; ``` ### No Custom Data Fetching in Primitives Base components can use `@tambo-ai/react` SDK hooks (components require Tambo provider anyway). **Custom data fetching logic** (combining sources, external providers) belongs in the styled layer. ```tsx // OK - SDK hooks in primitive const Root = ({ children }) => { const { value, setValue, submit } = useTamboThreadInput(); const { isIdle, cancel } = useTamboThread(); return <Context.Provider value={{ value, setValue, isIdle }}>{children}</Context.Provider>; }; // WRONG - custom data fetching in primitive const Textarea = ({ resourceProvider }) => { const { data: mcpResources } = useTamboMcpResourceList(search); const externalResources = useFetchExternal(resourceProvider); const combined = [...mcpResources, ...externalResources]; return <div>{combined.map(...)}</div>; }; ``` ### Pre-computed Props Arrays for Collections When exposing collections via render props, **pre-compute all props in a memoized array** rather than providing a getter function. ```tsx // AVOID - getter function pattern const Items = ({ children }) => { const { rawItems, selectedId, removeItem } = useContext(); const getItemProps = (index: number) => ({ /* new object every call */ }); return children({ items: rawItems, getItemProps }); }; // PREFERRED - pre-computed array const Items = ({ children }) => { const { rawItems, selectedId, removeItem } = useContext(); const items = React.useMemo<ItemRenderProps[]>( () => rawItems.map((item, index) => ({ item, index, isSelected: selectedId === item.id, onSelect: () => setSelectedId(item.id), onRemove: () => removeItem(item.id), })), [rawItems, selectedId, removeItem], ); return children({ items }); }; ``` ## Workflow Copy this checklist and track progress: ``` Compound Component Progress: - [ ] Step 1: Create context file - [ ] Step 2: Create Root component - [ ] Step 3: Create consumer components - [ ] Step 4: Create namespace export (index.tsx) - [ ] Step 5: Verify all guidelines met ``` ### Step 1: Create context file ``` my-component/ ├── index.tsx ├── component-context.tsx ├── component-root.tsx ├── component-item.tsx └── component-content.tsx ``` Create a context with a null default and a hook that throws on missing provider: ```tsx // component-context.tsx const ComponentContext = React.createContext<ComponentContextValue | null>( null, ); export function useComponentContext() { const context = React.useContext(ComponentContext); if (!context) { throw new Error("Component parts must be used within Component.Root"); } return context; } export { ComponentContext }; ``` ### Step 2: Create Root component Root manages state and provides context. Use `forwardRef`, support `asChild` via Radix `Slot`, and expose state via data attributes: ```tsx // component-root.tsx export const ComponentRoot = React.forwardRef< HTMLDivElement, ComponentRootProps >(({ asChild, defaultOpen = false, children, ...props }, ref) => { const [isOpen, setIsOpen] = React.useState(defaultOpen); const Comp = asChild ? Slot : "div"; return ( <ComponentContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }} > <Comp ref={ref} data-state={isOpen ? "open" : "closed"} {...props}> {children} </Comp> </ComponentContext.Provider> ); }); ComponentRoot.displayName = "Component.Root"; ``` ### Step 3: Create consumer components Choose the composition pattern based on need: **Direct children** (simplest, for static content): ```tsx const Content = ({ children, className, ...props }) => { const { data } = useComponentContext(); return ( <div className={className} {...props}> {children} </div> ); }; ``` **Render prop** (when consumer needs internal state): ```tsx const Content = ({ children, ...props }) => { const { data, isLoading } = useComponentContext(); const content = typeof children === "function" ? children({ data, isLoading }) : children; return <div {...props}>{content}</div>; }; ``` **Sub-context** (for lists where each item needs own context): ```tsx const Steps = ({ children }) => { const { reasoning } = useMessageContext(); return ( <StepsContext.Provider value={{ steps: reasoning }}> {children} </StepsContext.Provider> ); }; const Step = ({ children, index }) => { const { steps } = useStepsContext(); return ( <StepContext.Provider value={{ step: steps[index], index }}> {children} </StepContext.Provider> ); }; ``` ### Step 4: Create namespace export ```tsx // index.tsx export const Component = { Root: ComponentRoot, Trigger: ComponentTrigger, Content: ComponentContent, }; // Re-export types only - never hooks export type { ComponentRootProps } from "./component-root"; export type { ComponentContentProps } from "./component-content"; ``` ### Step 5: Verify guidelines - **No styles in primitives** - consumers control all styling via className/props - **Data attributes for CSS** - expose state like `data-state="open"`, `data-disabled`, `data-loading` - **Support asChild** - let consumers swap the underlying element via Radix `Slot` - **Forward refs** - always use `forwardRef` - **Display names** - set for DevTools (`Component.Root`, `Component.Item`) - **Throw on missing context** - fail fast with clear error messages - **Export types** - consumers need `ComponentProps`, `RenderProps` interfaces - **Hooks stay internal** - never export from index, expose state via render props - **SDK hooks OK, custom fetching not** - `@tambo-ai/react` hooks are fine, combining logic goes in styled layer - **Pre-compute collection props** - use `useMemo` arrays, not getter functions ## Pattern Selection | Scenario | Pattern | Why | | -------------------- | --------------- | ------------------------------- | | Static content | Direct children | Simplest, most flexible | | Need internal state | Render prop | Explicit state access | | List/iteration | Sub-context | Each item gets own context | | Element polymorphism | asChild | Change underlying element | | CSS-only styling | Data attributes | No JS needed for style variants | ## Anti-Patterns - **Hardcoded styles** - primitives should be unstyled - **Prop drilling** - use context instead - **Missing error boundaries** - throw when context is missing - **Inline functions in render prop types** - define proper interfaces - **Default exports** - use named exports in namespace object - **Exporting hooks** - hooks are internal; expose state via render props - **Custom data fetching in primitives** - SDK hooks are fine, but combining/external fetching belongs in styled layer - **Re-implementing base logic** - styled wrappers should compose, not duplicate - **Getter functions for collections** - pre-compute props arrays in useMemo instead.claude-plugin/marketplace.jsonmarketplaceShow content (598 bytes)
{ "name": "tambo-marketplace", "owner": { "name": "Tambo AI", "email": "support@tambo.co" }, "metadata": { "description": "Official Tambo plugins for Claude Code", "homepage": "https://tambo.co" }, "plugins": [ { "name": "tambo", "source": "./plugins/tambo", "description": "Build agents that speak your UI", "version": "1.0.0", "author": { "name": "Tambo AI", "email": "support@tambo.co" }, "category": "frameworks", "keywords": ["ai", "react", "generative-ui", "components", "streaming"] } ] }
README
Tambo AI
Build agents that speak your UI
The open-source generative UI toolkit for React. Connect your components—Tambo handles streaming, state management, and MCP.
Start For Free • Docs • Discord
Tambo 1.0 is here! Read the announcement: Introducing Tambo: Generative UI for React
Table of Contents
What is Tambo?
Tambo is a React toolkit for building agents that render UI (also known as generative UI).
Register your components with Zod schemas. The agent picks the right one and streams the props so users can interact with them. "Show me sales by region" renders your <Chart>. "Add a task" updates your <TaskBoard>.
https://github.com/user-attachments/assets/8381d607-b878-4823-8b24-ecb8053bef23
What's Included
Tambo is a fullstack solution for adding generative UI to your app. You get a React SDK plus a backend that handles conversation state and agent execution.
1. Agent included — Tambo runs the LLM conversation loop for you. Bring your own API key (OpenAI, Anthropic, Gemini, Mistral, or any OpenAI-compatible provider). Works with agent frameworks like LangChain and Mastra, but they're not required.
2. Streaming infrastructure — Props stream to your components as the LLM generates them. Cancellation, error recovery, and reconnection are handled for you.
3. Tambo Cloud or self-host — Cloud is a hosted backend that manages conversation state and agent orchestration. Self-hosted runs the same backend on your infrastructure via Docker.
Most software is built around a one-size-fits-all mental model. We built Tambo to help developers build software that adapts to users.
Get Started
npm create tambo-app my-tambo-app # auto-initializes git + tambo setup
cd my-tambo-app
npm run dev
Tambo Cloud is a hosted backend, free to get started with plenty of credits to start building. Self-hosted runs on your own infrastructure.
Check out the pre-built component library for agent and generative UI primitives:
https://github.com/user-attachments/assets/6cbc103b-9cc7-40f5-9746-12e04c976dff
Or fork a template:
| Template | Description |
|---|---|
| AI Chat with Generative UI | Chat interface with dynamic component generation |
| AI Analytics Dashboard | Analytics dashboard with AI-powered visualization |
How It Works
Tell the AI which components it can use. Zod schemas define the props. These schemas become LLM tool definitions—the agent calls them like functions and Tambo renders the result.
Generative Components
Render once in response to a message. Charts, summaries, data visualizations.
https://github.com/user-attachments/assets/3bd340e7-e226-4151-ae40-aab9b3660d8b
const components: TamboComponent[] = [
{
name: "Graph",
description: "Displays data as charts using Recharts library",
component: Graph,
propsSchema: z.object({
data: z.array(z.object({ name: z.string(), value: z.number() })),
type: z.enum(["line", "bar", "pie"]),
}),
},
];
Interactable Components
Persist and update as users refine requests. Shopping carts, spreadsheets, task boards.
https://github.com/user-attachments/assets/12d957cd-97f1-488e-911f-0ff900ef4062
const InteractableNote = withInteractable(Note, {
componentName: "Note",
description: "A note supporting title, content, and color modifications",
propsSchema: z.object({
title: z.string(),
content: z.string(),
color: z.enum(["white", "yellow", "blue", "green"]).optional(),
}),
});
Docs: generative components, interactable components
The Provider
Wrap your app with TamboProvider. You must provide either userKey or userToken to identify the thread owner.
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userKey={currentUserId}
components={components}
>
<Chat />
<InteractableNote id="note-1" title="My Note" content="Start writing..." />
</TamboProvider>
Use userKey for server-side or trusted environments. Use userToken (OAuth access token) for client-side apps where the token contains the user identity. See User Authentication for details.
Docs: provider options
Hooks
useTambo() is the primary hook — it gives you messages, streaming state, and thread management. useTamboThreadInput() handles user input and message submission.
const { messages, isStreaming } = useTambo();
const { value, setValue, submit, isPending } = useTamboThreadInput();
Docs: threads and messages, streaming status, full tutorial
Features
MCP Integrations
Connect to Linear, Slack, databases, or your own MCP servers. Tambo supports the full MCP protocol: tools, prompts, elicitations, and sampling.
import { MCPTransport } from "@tambo-ai/react/mcp";
const mcpServers = [
{
name: "filesystem",
url: "http://localhost:8261/mcp",
transport: MCPTransport.HTTP,
},
];
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userKey={currentUserId}
components={components}
mcpServers={mcpServers}
>
<App />
</TamboProvider>;
https://github.com/user-attachments/assets/c7a13915-8fed-4758-be1b-30a60fad0cda
Docs: MCP integration
Local Tools
Sometimes you need functions that run in the browser. DOM manipulation, authenticated fetches, accessing React state. Define them as tools and the AI can call them.
const tools: TamboTool[] = [
{
name: "getWeather",
description: "Fetches weather for a location",
tool: async (params: { location: string }) =>
fetch(`/api/weather?q=${encodeURIComponent(params.location)}`).then((r) =>
r.json(),
),
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
condition: z.string(),
location: z.string(),
}),
},
];
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userKey={currentUserId}
tools={tools}
components={components}
>
<App />
</TamboProvider>;
Docs: local tools
Context, Auth, and Suggestions
Additional context lets you pass metadata to give the AI better responses. User state, app settings, current page. User authentication passes tokens from your auth provider. Suggestions generates prompts users can click based on what they're doing.
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userToken={userToken}
contextHelpers={{
selectedItems: () => ({
key: "selectedItems",
value: selectedItems.map((i) => i.name).join(", "),
}),
currentPage: () => ({ key: "page", value: window.location.pathname }),
}}
/>
const { suggestions, accept } = useTamboSuggestions({ maxSuggestions: 3 });
suggestions.map((s) => (
<button key={s.id} onClick={() => accept(s)}>
{s.title}
</button>
));
Docs: additional context, user authentication, suggestions
Supported LLM Providers
OpenAI, Anthropic, Cerebras, Google Gemini, Mistral, and any OpenAI-compatible provider. Full list. Missing one? Let us know.
How Tambo Compares
| Feature | Tambo | Vercel AI SDK | CopilotKit | Assistant UI |
|---|---|---|---|---|
| Component selection | AI decides which components to render | Manual tool-to-component mapping | Via agent frameworks (LangGraph) | Chat-focused tool UI |
| MCP integration | Built-in | Experimental (v4.2+) | Recently added | Requires AI SDK v5 |
| Persistent stateful components | Yes | No | Shared state patterns | No |
| Client-side tool execution | Declarative, automatic | Manual via onToolCall | Agent-side only | No |
| Self-hostable | MIT (SDK + backend) | Apache 2.0 (SDK only) | MIT | MIT |
| Hosted option | Tambo Cloud | No | CopilotKit Cloud | Assistant Cloud |
| Best for | Full app UI control | Streaming and tool abstractions | Multi-agent workflows | Chat interfaces |
Community
Join the Discord to chat with other developers and the core team.
Interested in contributing? Read the Contributing Guide.
Join the conversation on Twitter and follow @tambo_ai.
License
MIT unless otherwise noted. Some workspaces (like apps/api) are Apache-2.0.
For AI/LLM agents: docs.tambo.co/llms.txt