Prebuilt Chat UI
Cascaide templates ship with a prebuilt Chat UI built with React. It is easily hackable.
It’s more than just a UI, it’s a participant in the graph. It handles initiating cascades and observing it, We also leverage it to initiate cascades for sub agent delegations. This pattern of using the client to handle sub agent delegations is especially useful in serverless environments to preserve graceful timeout handling.
What we hope to convey here are patterns. Treat this as an example implementation to draw inspiration from or to modify.
Check out the code at https://github.com/cascaide-ts/cascaide-nextjs-starter/tree/main/components/cascaide-ui/chat
Installation Guide
You can install it into your project using
npx shadcn@latest add https://raw.githubusercontent.com/cascaide-ts/cascaide-ui/main/registry/chat.jsonOverview
This is a prebuilt chat interface built on top of cascaide-ts, a multi-agent orchestration framework. It supports three agent types out of the box — a hotel booking supervisor, a ReAct search agent, and a recursive deep-search agent — and includes full support for human-in-the-loop (HITL) tool UIs, sub-cascade delegation, and streaming conversation state.
File Structure
├── chat.tsx # Root component — layout, state, message dispatch
├── input.tsx # InputBar — textarea, send button, agent cards
├── message-bubble.tsx # Renders individual messages (text, reasoning, tool calls)
├── message-list.tsx # Conversation scroll view + delegation orchestration
├── sidebar.tsx # Collapsible navigation sidebar with chat history
└── tool-ui/
└── hotel-tool.tsx # Example HITL tool: hotel selection UIComponents
Chat (chat.tsx)
The root component. Manages global state and wires together all child components.
Props
| Prop | Type | Description |
|---|---|---|
nodeId | string | The workflow node ID used to register active nodes via useWorkflow |
Key responsibilities
- Maintains the active
chatId(a UUID, reset on agent change or new chat) - Subscribes to cascade state via
useCascade(chatId) - Derives
conversationMessagesfromcascadeState.history, with an optimistic pending user message spliced in while the cascade catches up - Dispatches messages by building a
Spawnsobject and callingaddActiveNode - Handles tool responses from HITL tools via
handleToolResponse
Agent types
type AgentType =
| 'hotelSupervisorNode'
| 'searchAgentNode'
| 'recursiveSearchAgentNode';Switching agents resets chatId and clears conversation state. The selected agent determines which key is used in the Spawns object when sending a message.
InputBar (input.tsx)
The text input area shown both on the empty-state landing screen and in the active conversation footer.
Props
| Prop | Type | Description |
|---|---|---|
input | string | Controlled textarea value |
isProcessing | boolean | Disables input while a cascade is running |
userId | string | Disables input if falsy |
compact | boolean | Reduces padding and hides agent cards (used in the footer) |
onChange | (value: string) => void | Updates input state |
onSend | () => void | Triggered on button click |
onKeyPress | (e: KeyboardEvent) => void | Triggered on Enter (Shift+Enter inserts a newline) |
Note: The
InputBarininput.tsxdeclaresselectedAgentandonAgentChangeprops but these are not yet wired up — agent switching is currently handled inchat.tsxdirectly. TheAGENTSarray and the olderPROMPT_SUGGESTIONSarray both exist in the file;PROMPT_SUGGESTIONSis what’s actually rendered in the suggestion cards.
Agent suggestion cards
In non-compact mode, three clickable prompt cards are shown below the input. Clicking one populates the textarea and immediately sends the message. These map to the three agent types and auto-fill representative starter prompts.
MessageList (message-list.tsx)
Renders the full conversation history and orchestrates sub-cascade delegation.
Props
| Prop | Type | Description |
|---|---|---|
displayHistory | CanonicalMessage[] | Full conversation history to render |
userId | string | undefined | Passed through to MessageBubble |
addActiveNode | function | Used to spawn sub-cascades for delegation |
handleToolResponse | function | Called when a HITL tool or sub-cascade completes |
Tool registry
export const toolRegistry: Record<string, React.ComponentType<ToolComponentProps>> = {
present_hotel_options: HotelOptions,
};Register your own HITL tool components here. The key must match the tool name the LLM uses in its tool call.
Delegation flow
When the supervisor agent calls a tool named delegate_to_<agentName>, MessageList automatically:
- Detects the delegation call in the latest assistant message
- Spawns a new sub-cascade with a fresh UUID (
sub_cascade_<uuid>) - Mounts a
CascadeMonitorthat subscribes to the sub-cascade - When the sub-cascade completes, calls
handleToolResponsewith the result, which sends it back to the supervisor as a tool result message
Allowed delegation targets:
const ALLOWED_AGENTS = ['availabilityAgentNode', 'bookingAgentNode'];MessageBubble (message-bubble.tsx)
Renders a single message. Uses an internal block registry to dispatch different content types.
Content blocks (in order)
| Block | Renders when |
|---|---|
thinking | message.thinking is present — collapsible reasoning panel |
content | message.content is present — Markdown-rendered text |
tool_calls | message.tool_calls is present — tool call cards |
User messages render as a right-aligned gray pill with a “U” avatar. Tool result messages (role === 'tool') are not rendered directly — they are looked up and associated with the corresponding tool call.
Tool call rendering
Each tool call is rendered by ToolCallRenderer. If the tool name exists in toolRegistry, the registered component is used. Otherwise, a generic collapsible card shows the tool name, a running/executed badge, and the raw JSON args.
Sidebar (sidebar.tsx)
A collapsible navigation panel.
Props
| Prop | Type | Description |
|---|---|---|
history | any[] | Array of past chat objects with id, title, messages, lastUpdated |
currentChatId | string | Highlights the active chat |
onNewChat | () => void | Starts a fresh conversation |
onSelectChat | (id: string) => void | Switches to an existing conversation |
isOpen | boolean | Controls mobile visibility |
onClose | () => void | Closes on mobile |
isExpanded | boolean | Wide (label visible) vs. narrow (icon-only) on desktop |
toggleExpansion | () => void | Toggles expanded state |
Note:
onSelectChatis a stub — loading historical conversation state is marked TODO inchat.tsx.
HotelOptions (tool-ui/hotel-tool.tsx)
The canonical example of a HITL (Human-in-the-Loop) input tool.
How it works
- The LLM calls the
present_hotel_optionstool with a list of hotels MessageListroutes this toHotelOptionsviatoolRegistry- The user selects a hotel and room type
onCompleteis called with a result string:"selected [Hotel Name] and [Room Type]"- The cascade resumes with this as the tool result
Props (via ToolComponentProps)
| Prop | Type | Description |
|---|---|---|
args | Record<string, any> | LLM-provided tool arguments; expects args.hotels[] |
onComplete | (result: string) => void | Call when user makes a selection |
isFinished | boolean | True after a selection has been made — makes the UI read-only |
savedResult | string | null | Re-parsed to highlight the previously selected option on reload |
Building your own tool
- Create a component matching
ToolComponentProps - Call
onComplete(yourResultString)when the user acts - Use
isFinishedto make the UI read-only - Register it in
toolRegistryinmessage-list.tsx
ChartRenderer (chart-renderer.tsx)
A standalone chart display component built on Recharts. Not currently wired into the agent tool registry, but ready to register as a tool UI.
Supported chart types: line, bar, area, pie
Props (ChartDisplayProps)
| Prop | Type | Description |
|---|---|---|
chartType | string | One of line, bar, area, pie |
title | string | Card title |
data | any[] | Recharts-compatible data array |
xAxisKey | string | Key used for the X-axis / pie name |
dataKeys | string[] | Keys for each series or the pie value |
colors | string[] | Hex color array, cycled across series |
userId | string | User identifier |
id | string | Record ID (for future persistence) |
Data Flow
User types → handleSendMessage()
→ setPendingUserMessage() (optimistic UI)
→ addActiveNode(spawns) (dispatches to cascaide-ts)
→ useCascade(chatId) updates (streaming state)
→ conversationMessages derived from cascadeState.history
→ MessageList renders messages
→ tool call detected → HITL tool rendered
→ user acts → onComplete() called
→ handleToolResponse() → addActiveNode() resumes cascadeFor delegated sub-agents:
LLM calls delegate_to_<agent>
→ MessageList detects delegation call
→ new sub-cascade spawned (sub_cascade_<uuid>)
→ CascadeMonitor mounts, subscribes via useCascade(subCascadeId)
→ sub-cascade completes
→ handleDelegationComplete() called
→ supervisor receives tool result → cascade resumesDependencies
| Package | Purpose |
|---|---|
@cascaide-ts/react | useWorkflow, useCascade hooks |
@cascaide-ts/core | Spawns type |
@cascaide-ts/helpers | CanonicalMessage type |
react-markdown | Markdown rendering in messages |
remark-gfm | GitHub Flavored Markdown |
remark-math + rehype-katex | LaTeX math rendering |
rehype-highlight | Code block syntax highlighting |
recharts | Charts in ChartRenderer |
lucide-react | Icons |
uuid | Chat and sub-cascade ID generation |