# Building Settings UI

> 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.

- Skill: `tambo-ai/building-settings-ui` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tambo-ai/building-settings-ui`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tambo-ai/building-settings-ui/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tambo-ai (https://skillmd.com/u/tambo-ai)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/tambo-ai/building-settings-ui

---


# 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.

