Files
composio-skills-reference/composio-sdk/rules/tr-building-chat-ui.md
sohamganatra b8b711dff6 Add Composio SDK skill with rules and agent config
Adds composio-sdk/ with SKILL.md, AGENTS.md, and 18 rule files
covering Tool Router, direct execution, triggers, and auth patterns.

Source: composiohq/skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 22:54:21 -08:00

9.3 KiB

title, impact, description, tags
title impact description tags
Building Chat UIs with Tool Router HIGH Best practices for building chat applications with toolkit selection, connection management, and session handling
tool-router
chat-ui
vercel-ai-sdk
toolkit-selection
authentication
session

Building Chat UIs with Tool Router

Build chat applications with Tool Router using Vercel AI SDK, create sessions per message with dynamic configuration, and provide toolkit selection and connection management UI.

  • Native streaming support
  • React hooks for chat interfaces
  • Built-in UI components
  • Excellent DX with Tool Router

Incorrect - Sharing Sessions Without Config

// DON'T: Reuse sessions without proper configuration
const globalSession = await composio.create('default', {
  toolkits: ['gmail'] // Hard-coded toolkits
});

app.post('/api/chat', async (req, res) => {
  // ❌ No user isolation
  // ❌ No per-message configuration
  // ❌ Can't change toolkits dynamically
  const tools = await globalSession.tools();
});

Correct - Session Per Message

// DO: Create sessions per message with proper config
import { Composio } from '@composio/core';
import { VercelProvider } from '@composio/vercel';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

const composio = new Composio({ provider: new VercelProvider() });

app.post('/api/chat', async (req, res) => {
  const { userId, message, selectedToolkits } = req.body;

  // Create new session for this message
  const session = await composio.create(userId, {
    toolkits: selectedToolkits, // User-selected toolkits
    manageConnections: true
  });

  const tools = await session.tools();

  const stream = await streamText({
    model: openai('gpt-4o'),
    messages: [{ role: 'user', content: message }],
    tools,
    maxSteps: 10
  });

  return stream.toDataStreamResponse();
});

Toolkit Selection UI

List All Available Toolkits

Create a session without toolkit filters to show all available toolkits:

// API endpoint to list all toolkits
app.post('/api/toolkits', async (req, res) => {
  const { userId } = req.body;

  // No toolkits parameter = all toolkits available
  const session = await composio.create(userId);
  const toolkits = await session.toolkits();

  res.json(toolkits.map(tk => ({
    slug: tk.slug,
    name: tk.name,
    description: tk.description,
    logo: tk.logo,
    isConnected: tk.connectedAccounts.length > 0
  })));
});

React Component

export function ToolkitSelector({ userId, onSelect }: Props) {
  const [toolkits, setToolkits] = useState<Toolkit[]>([]);
  const [selected, setSelected] = useState<string[]>([]);

  useEffect(() => {
    fetch('/api/toolkits', {
      method: 'POST',
      body: JSON.stringify({ userId })
    }).then(res => res.json()).then(setToolkits);
  }, [userId]);

  return (
    <div className="toolkit-grid">
      {toolkits.map(tk => (
        <div
          key={tk.slug}
          className={selected.includes(tk.slug) ? 'selected' : ''}
          onClick={() => setSelected(prev =>
            prev.includes(tk.slug) ? prev.filter(s => s !== tk.slug) : [...prev, tk.slug]
          )}
        >
          <img src={tk.logo} alt={tk.name} />
          <h3>{tk.name}</h3>
          {tk.isConnected && <span> Connected</span>}
        </div>
      ))}
      <button onClick={() => onSelect(selected)}>Use Selected</button>
    </div>
  );
}

Connection Management UI

Authorize Toolkits

// API endpoint to start connection flow
app.post('/api/connect', async (req, res) => {
  const { userId, toolkitSlug } = req.body;

  const session = await composio.create(userId, {
    toolkits: [toolkitSlug]
  });

  const auth = await session.authorize({
    toolkit: toolkitSlug,
    redirectUrl: `${process.env.APP_URL}/auth/callback`
  });

  res.json({ redirectUrl: auth.redirectUrl });
});

React Component

export function ConnectedAccounts({ userId }: Props) {
  const [toolkits, setToolkits] = useState<Toolkit[]>([]);

  const handleConnect = async (slug: string) => {
    const res = await fetch('/api/connect', {
      method: 'POST',
      body: JSON.stringify({ userId, toolkitSlug: slug })
    });
    const { redirectUrl } = await res.json();
    window.location.href = redirectUrl;
  };

  return (
    <div>
      {toolkits.map(tk => (
        <div key={tk.slug}>
          <h3>{tk.name}</h3>
          {tk.isConnected ? (
            <button onClick={() => handleDisconnect(tk.slug)}>Disconnect</button>
          ) : (
            <button onClick={() => handleConnect(tk.slug)}>Connect</button>
          )}
        </div>
      ))}
    </div>
  );
}

Connected Account Sharing

Connected accounts are shared between sessions (tied to user ID and auth configs, not individual sessions).

// Both sessions use the same Gmail connected account
const session1 = await composio.create('user_123', { toolkits: ['gmail'] });
const session2 = await composio.create('user_123', { toolkits: ['gmail', 'slack'] });

// ✅ Connected accounts shared across sessions
// ✅ No need to reconnect for each session

Override Connected Accounts

// Override which connected account to use
const session = await composio.create('user_123', {
  toolkits: ['gmail'],
  connectedAccounts: {
    gmail: 'conn_specific_account_id' // Use specific account
  }
});

Override Auth Config

// Override which auth config to use
const session = await composio.create('user_123', {
  toolkits: ['gmail'],
  authConfig: {
    gmail: 'auth_config_custom_id' // Use custom auth config
  }
});

Complete Chat Application

// app/api/chat/route.ts
import { Composio } from '@composio/core';
import { VercelProvider } from '@composio/vercel';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

const composio = new Composio({ provider: new VercelProvider() });

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

  const session = await composio.create(userId, {
    toolkits: selectedToolkits,
    manageConnections: true
  });

  const tools = await session.tools();

  const result = await streamText({
    model: openai('gpt-4o'),
    messages,
    tools,
    maxSteps: 10
  });

  return result.toDataStreamResponse();
}
// app/page.tsx - Chat UI
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';

export default function ChatPage() {
  const [selectedToolkits, setSelectedToolkits] = useState(['gmail']);

  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat',
    body: { userId: 'user_123', selectedToolkits }
  });

  return (
    <div>
      <ToolkitSelector
        userId="user_123"
        selected={selectedToolkits}
        onSelect={setSelectedToolkits}
      />
      <div className="messages">
        {messages.map(m => (
          <div key={m.id} className={m.role}>{m.content}</div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Manual Tool Operations (Advanced)

For custom workflows, you can manually fetch and execute tools instead of using sessions.

Manual Tool Fetching

// Fetch raw tool metadata
const tools = await composio.tools.getRawComposioTools({
  toolkits: ['gmail', 'slack'],
  important: true
});

Manual Tool Execution

// Execute tools directly
const result = await composio.tools.execute('GMAIL_SEND_EMAIL', {
  userId: 'user_123',
  arguments: { to: 'test@example.com', subject: 'Hello' },
  version: '15082025_00' // Version REQUIRED for manual execution
});

if (!result.successful) {
  console.error('Failed:', result.error);
}

When to Use Manual Approach

Use Case Recommended Approach
Chat UIs, agents, streaming session.tools()
Custom workflows, catalogs Manual fetch/execute

Reference: See Fetching Tools and Tool Execution for detailed manual operation guides.

Best Practices

  1. Create Sessions Per Message - Fresh session with config for each interaction
  2. Let Users Select Toolkits - Dynamic toolkit configuration via UI
  3. Show Connection Status - Display which toolkits are connected
  4. Handle Authorization - Use session.authorize() for auth flows
  5. Enable Connection Management - Set manageConnections: true

Key Principles

  1. Vercel AI SDK - Best framework for chat UIs
  2. Session per message - Fresh sessions with config
  3. No toolkit filter - List all by creating session without toolkits
  4. Shared connections - Connected accounts shared across sessions
  5. Override when needed - Use connectedAccounts or authConfig for special cases

Reference