Architecture
The frontend uses the Vercel AI SDK’suseChat 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);
// app/api/tasks/route.ts
import { TerminalUseClient } from '@terminaluse/sdk';
const client = new TerminalUseClient({
environment: process.env.TERMINALUSE_BASE_URL ?? 'https://api.terminaluse.com',
bearerAuth: { token: process.env.TERMINALUSE_API_KEY! },
});
export async function POST(request: Request) {
const { agentName, filesystemId } = await request.json();
const task = await client.tasks.create({
agent_name: agentName,
filesystem_id: filesystemId,
});
return Response.json(task);
}
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 } }
);
// app/api/chat/route.ts
import { streamText, convertToModelMessages, type UIMessage } from 'ai';
import { createTerminalUseProvider } from '@terminaluse/vercel-ai-sdk-provider';
const tu = createTerminalUseProvider({
apiKey: process.env.TERMINALUSE_API_KEY!,
baseURL: process.env.TERMINALUSE_BASE_URL!,
});
export async function POST(req: Request) {
const { messages, agentName, taskId } = await req.json();
const result = streamText({
model: tu.agent(agentName),
messages: await convertToModelMessages(messages as UIMessage[]),
providerOptions: {
terminaluse: { taskId },
},
});
return result.toUIMessageStreamResponse();
}
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
}
// app/api/tasks/[taskId]/messages/route.ts
import { TerminalUseClient } from '@terminaluse/sdk';
const client = new TerminalUseClient({
environment: process.env.TERMINALUSE_BASE_URL ?? 'https://api.terminaluse.com',
bearerAuth: { token: process.env.TERMINALUSE_API_KEY! },
});
export async function GET(
request: Request,
{ params }: { params: Promise<{ taskId: string }> }
) {
const { taskId } = await params;
const response = await client.messagesV2.list({
task_id: taskId,
direction: 'newer',
});
return Response.json(response.data);
}
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
| Variable | Required | Default | Description |
|---|---|---|---|
TERMINALUSE_API_KEY | Yes | — | Your API key from the platform |
TERMINALUSE_BASE_URL | No | https://api.terminaluse.com | API base URL (override for self-hosted or staging) |