Skip to main content

Architecture

The frontend uses the Vercel AI SDK’s useChat hook. API routes proxy requests to Terminal Use and stream responses back.

Setup

npm install @terminaluse/vercel-ai-sdk-provider

Creating Tasks

// Create task on first message
const response = await fetch('/api/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ agentName: AGENT_NAME, filesystemId }),
});
const task = await response.json();
setTaskId(task.id);

Sending Messages

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

const AGENT_NAME = 'my-namespace/my-agent';

// Configure transport with task context
const transport = useMemo(
  () => new DefaultChatTransport({
    api: '/api/chat',
    body: { agentName: AGENT_NAME, taskId },
  }),
  [taskId]
);

const { messages, sendMessage, status } = useChat({
  id: 'chat-session', // Client-generated session ID
  transport,
});

// Send message with task context
await sendMessage(
  { text: inputValue },
  { body: { agentName: AGENT_NAME, taskId } }
);

Loading Message History

When resuming an existing task, fetch and display previous messages.
import { useQuery } from '@tanstack/react-query';
import { useLayoutEffect } from 'react';

function ExistingTaskChat({ taskId }: { taskId: string }) {
  const { data: history, isLoading } = useQuery({
    queryKey: ['taskMessages', taskId],
    queryFn: async () => {
      const response = await fetch(`/api/tasks/${taskId}/messages`);
      return response.json();
    },
  });

  const { messages, setMessages } = useChat({ id: taskId, transport });

  // Initialize with history
  useLayoutEffect(() => {
    if (history) setMessages(history);
  }, [history, setMessages]);

  if (isLoading) return <div>Loading...</div>;

  // ... render chat interface
}

Handling Message Parts

The following is a sample implementation. See the Vercel AI SDK documentation for more details on displaying UIMessage parts.
import { isTextUIPart, isReasoningUIPart, isToolUIPart } from 'ai';
import type { UIMessage, ToolUIPart } from 'ai';

function MessagePart({ part }: { part: UIMessage['parts'][number] }) {
  if (isTextUIPart(part)) {
    return <p>{part.text}</p>;
  }

  if (isReasoningUIPart(part)) {
    return (
      <div className="italic text-gray-600">
        <span className="font-medium">Reasoning:</span> {part.text}
      </div>
    );
  }

  if (isToolUIPart(part)) {
    const toolPart = part as ToolUIPart;
    return (
      <div className="my-2 p-3 border rounded">
        <div className="font-medium">{toolPart.toolName}</div>
        <pre className="text-sm">{JSON.stringify(toolPart.input, null, 2)}</pre>
        {toolPart.output && (
          <pre className="text-sm">{JSON.stringify(toolPart.output, null, 2)}</pre>
        )}
      </div>
    );
  }

  return null;
}

Environment Variables

TERMINALUSE_API_KEY=your_api_key
TERMINALUSE_BASE_URL=https://api.terminaluse.com  # Optional, defaults to production
VariableRequiredDefaultDescription
TERMINALUSE_API_KEYYesYour API key from the platform
TERMINALUSE_BASE_URLNohttps://api.terminaluse.comAPI base URL (override for self-hosted or staging)