threads
npx machina-cli add skill andrmaz/spec-driven-architecture/threads --openclawThreads and Input
Manages conversations, suggestions, voice input, and image attachments.
Quick Start
import { useTambo, useTamboThreadInput } from "@tambo-ai/react";
const { thread, messages, isIdle } = useTambo();
const { value, setValue, submit } = useTamboThreadInput();
await submit(); // sends current input value
Thread Management
Access and manage the current thread using useTambo() and useTamboThreadInput():
import {
useTambo,
useTamboThreadInput,
ComponentRenderer,
} from "@tambo-ai/react";
function Chat() {
const {
thread, // Current thread state
messages, // Messages with computed properties
isIdle, // True when not generating
isStreaming, // True when streaming response
isWaiting, // True when waiting for server
currentThreadId, // Active thread ID
switchThread, // Switch to different thread
startNewThread, // Create new thread, returns ID
cancelRun, // Cancel active generation
} = useTambo();
const {
value, // Current input value
setValue, // Update input
submit, // Send message
isPending, // Submission in progress
images, // Staged image files
addImage, // Add single image
removeImage, // Remove image by ID
} = useTamboThreadInput();
const handleSend = async () => {
await submit();
};
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.content.map((block) => {
switch (block.type) {
case "text":
return <p key={block.type}>{block.text}</p>;
case "component":
return (
<ComponentRenderer
key={block.id}
content={block}
threadId={currentThreadId}
messageId={msg.id}
/>
);
case "tool_use":
return (
<div key={block.id}>
{block.statusMessage ?? `Running ${block.name}...`}
</div>
);
default:
return null;
}
})}
</div>
))}
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button onClick={handleSend} disabled={!isIdle || isPending}>
Send
</button>
</div>
);
}
Streaming State
| Property | Type | Description |
|---|---|---|
isIdle | boolean | Not generating |
isWaiting | boolean | Waiting for server response |
isStreaming | boolean | Actively streaming response |
The streamingState object provides additional detail:
const { streamingState } = useTambo();
// streamingState.status: "idle" | "waiting" | "streaming"
// streamingState.runId: current run ID
// streamingState.error: { message, code } if error occurred
Content Block Types
Messages contain an array of content blocks. Handle each type:
| Type | Description | Key Fields |
|---|---|---|
text | Plain text | text |
component | AI-generated component | id, name, props |
tool_use | Tool invocation | id, name, input |
tool_result | Tool response | tool_use_id, content |
resource | MCP resource | uri, name, text |
Submit Options
const { submit } = useTamboThreadInput();
await submit({
threadId: "specific-thread", // Override target thread
toolChoice: "auto", // "auto" | "required" | "none" | { name: "toolName" }
maxTokens: 4096, // Max response tokens
systemPrompt: "Be helpful", // Override system prompt
});
Fetching a Thread by ID
To fetch a specific thread (e.g., for a detail view), use useTamboThread(threadId):
import { useTamboThread } from "@tambo-ai/react";
function ThreadView({ threadId }: { threadId: string }) {
const { data: thread, isLoading, isError } = useTamboThread(threadId);
if (isLoading) return <Skeleton />;
if (isError) return <div>Failed to load thread</div>;
return <div>{thread.name}</div>;
}
This is a React Query hook - use it for read-only thread fetching, not for the active conversation.
Thread List
Manage multiple conversations:
import { useTambo, useTamboThreadList } from "@tambo-ai/react";
function ThreadSidebar() {
const { data, isLoading } = useTamboThreadList();
const { currentThreadId, switchThread, startNewThread } = useTambo();
if (isLoading) return <Skeleton />;
return (
<div>
<button onClick={() => startNewThread()}>New Thread</button>
<ul>
{data?.threads.map((t) => (
<li key={t.id}>
<button
onClick={() => switchThread(t.id)}
className={currentThreadId === t.id ? "active" : ""}
>
{t.name || "Untitled"}
</button>
</li>
))}
</ul>
</div>
);
}
Thread List Options
const { data } = useTamboThreadList({
userKey: "user_123", // Filter by user (defaults to provider's userKey)
limit: 20, // Max results
cursor: nextCursor, // Pagination cursor
});
// data.threads: TamboThread[]
// data.hasMore: boolean
// data.nextCursor: string
Suggestions
AI-generated follow-up suggestions after each assistant message:
import { useTamboSuggestions } from "@tambo-ai/react";
function Suggestions() {
const { suggestions, isLoading, accept, isAccepting } = useTamboSuggestions({
maxSuggestions: 3, // 1-10, default 3
autoGenerate: true, // Auto-generate after assistant message
});
if (isLoading) return <Skeleton />;
return (
<div className="suggestions">
{suggestions.map((s) => (
<button
key={s.id}
onClick={() => accept({ suggestion: s })}
disabled={isAccepting}
>
{s.title}
</button>
))}
</div>
);
}
Auto-Submit Suggestion
// Accept and immediately submit as a message
accept({ suggestion: s, shouldSubmit: true });
Manual Generation
const { generate, isGenerating } = useTamboSuggestions({
autoGenerate: false, // Disable auto-generation
});
<button onClick={() => generate()} disabled={isGenerating}>
Get suggestions
</button>;
Voice Input
Speech-to-text transcription:
import { useTamboVoice } from "@tambo-ai/react";
function VoiceButton() {
const {
startRecording,
stopRecording,
isRecording,
isTranscribing,
transcript,
transcriptionError,
mediaAccessError,
} = useTamboVoice();
return (
<div>
<button onClick={isRecording ? stopRecording : startRecording}>
{isRecording ? "Stop" : "Record"}
</button>
{isTranscribing && <span>Transcribing...</span>}
{transcript && <p>{transcript}</p>}
{transcriptionError && <p className="error">{transcriptionError}</p>}
</div>
);
}
Voice Hook Returns
| Property | Type | Description |
|---|---|---|
startRecording | () => void | Start recording, reset transcript |
stopRecording | () => void | Stop and start transcription |
isRecording | boolean | Currently recording |
isTranscribing | boolean | Processing audio |
transcript | string | null | Transcribed text |
transcriptionError | string | null | Transcription error |
mediaAccessError | string | null | Mic access error |
Image Attachments
Images are managed via useTamboThreadInput():
import { useTamboThreadInput } from "@tambo-ai/react";
function ImageInput() {
const { images, addImage, addImages, removeImage, clearImages } =
useTamboThreadInput();
const handleFiles = async (files: FileList) => {
await addImages(Array.from(files));
};
return (
<div>
<input
type="file"
accept="image/*"
multiple
onChange={(e) => handleFiles(e.target.files!)}
/>
{images.map((img) => (
<div key={img.id}>
<img src={img.dataUrl} alt={img.name} />
<button onClick={() => removeImage(img.id)}>Remove</button>
</div>
))}
</div>
);
}
StagedImage Properties
| Property | Type | Description |
|---|---|---|
id | string | Unique image ID |
name | string | File name |
dataUrl | string | Base64 data URL |
file | File | Original File object |
size | number | File size in bytes |
type | string | MIME type |
User Authentication
Enable per-user thread isolation:
import { TamboProvider } from "@tambo-ai/react";
function App() {
return (
<TamboProvider
apiKey={apiKey}
userKey="user_123" // Simple user identifier
>
<Chat />
</TamboProvider>
);
}
For OAuth-based auth, use userToken instead:
function App() {
const userToken = useUserToken(); // From your auth provider
return (
<TamboProvider apiKey={apiKey} userToken={userToken}>
<Chat />
</TamboProvider>
);
}
Use userKey for simple user identification or userToken for OAuth JWT tokens. Don't use both.
Source
git clone https://github.com/andrmaz/spec-driven-architecture/blob/develop/.agents/skills/threads/SKILL.mdView on GitHub Overview
Threads manages conversations, messages, AI suggestions, voice input, and image attachments across multi-thread UIs. It exposes hooks like useTambo and useTamboThreadInput to access thread state, send messages, and control attachments and voice features.
How This Skill Works
Use useTambo to access the current thread and messages, and use useTamboThreadInput to manage user input, images, and submission. Messages are composed of content blocks (text, component, tool_use, etc.) that you render based on their type, while streaming and idle flags indicate generation status.
When to Use It
- Building a multi-thread chat UI where users switch between conversations
- Sending messages with AI-generated suggestions or tool invocations
- Adding voice input and staging image attachments within messages
- Managing thread lifecycle: creating new threads and switching active ones
- Monitoring streaming, waiting, and idle states during generation
Quick Start
- Step 1: Import hooks from @tambo-ai/react
- Step 2: Use const { thread, messages, isIdle } = useTambo(); const { value, setValue, submit } = useTamboThreadInput();
- Step 3: Call await submit(); to send the current input value
Best Practices
- Keep threadId and currentThreadId synchronized when switching threads
- Use isIdle, isStreaming, and isWaiting to drive loading states in the UI
- Validate and handle images via images, addImage, and removeImage before submitting
- Render content blocks by type (text, component, tool_use, etc.) to ensure correct UI
- Handle streamingState and errors gracefully to provide clear feedback
Example Use Cases
- A customer-support dashboard that maintains multiple chat threads with AI-assisted suggestions
- A content drafting app that inserts AI components and tool results within messages
- A voice-enabled chat where users can attach images and receive streaming responses
- A collaborative chat where users switch between product discussion threads in real time
- An admin panel that creates and manages threads for different customer inquiries