AI Docs
Chat & Streaming
Transports
Tools
Middleware
Chat UI Recipes
Interrupts
MCP
Resumable Streams
Persistence
Persistence (Advanced)
Media
Embeddings
Reranking
Evaluate
Code Mode
Computer Use
Harness
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
Sandboxes
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
Advanced
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
AI/LLM: This documentation page is available in plain markdown format at/ai/latest/docs/ui/recipes/custom-interrupt.md
Chat UI Recipes
Ask the user your own question
defineInterrupt pauses the run for a question that is not a tool approval. The component sends the answer.
Two schemas define the exchange:
- payloadSchema: what the server sends to you.
- responseSchema: what you send back.
This page covers the UI. To pause a run on the server, read Generic interrupts.
Full example#
- Define the interrupt. The server sends a list of plan names. You send one name back.
tsx
import { defineInterrupt } from '@tanstack/ai'
import { z } from 'zod'
export const choosePlan = defineInterrupt({
id: 'choosePlan',
payloadSchema: z.object({
options: z.array(z.string()),
}),
responseSchema: z.string(),
})- Add it to interrupts in chatOptions. Register a component under its id in interruptsComponents.generic, and render Interrupts in the layout:
tsx
import { fetchServerSentEvents } from '@tanstack/ai-react'
import { createChatHook } from '@tanstack/ai-react/ui'
const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
interrupts: [choosePlan],
}
const { useAppChat } = createChatHook({
options: chatOptions,
components: {
layout: ({ Messages, Interrupts }) => (
<main>
<Messages />
<Interrupts />
</main>
),
message: ({ Parts }) => <article><Parts /></article>,
},
partsComponents: {
text: ({ part }) => <p>{part.content}</p>,
fallback: () => null,
},
interruptsComponents: {
generic: {
choosePlan: ({ interrupt }) => (
<fieldset>
<legend>Pick a plan</legend>
{interrupt.payload?.options.map((plan) => (
<button key={plan} onClick={() => interrupt.resolveInterrupt(plan)}>
{plan}
</button>
))}
</fieldset>
),
fallback: ({ interrupt }) => <p>{interrupt.reason}</p>,
},
},
})
export function PlanChat() {
const chat = useAppChat({ threadId: 'support-chat' })
return <chat.AppChat />
}When the server asks, the plan buttons show in the interrupt list.
How it works#
- interrupt.payload matches your payloadSchema, so options is a string array.
- resolveInterrupt accepts only what your responseSchema allows. Passing a number here is a compile error.
- No tool owns a generic interrupt, so it always renders in Interrupts. If the layout does not render Interrupts, the question never shows.
Always register a fallback#
fallback catches two things:
- An interrupt id that you did not register.
- An unbound interrupt. Something outside this chat owns it, so it cannot be answered here.
If the copy must be different, branch on interrupt.kind === 'unbound'.
Next#
- Approving a tool instead? Read Ask before a tool runs.