In Getting Started you built a weather tool that returns data to the AI, and the AI formats it as text. That works, but real applications need real UI — confirmation dialogs, data cards, forms, preference pickers.
The display stack is how tools show UI to the user. Instead of returning raw data to the AI, a tool can push a React component onto a stack that your app renders. This guide walks through how to use it.
Every tool's do function receives two arguments: the validated input and a display object. The display object has two methods:
display.pushAndForget({ input }) — push a component and keep the tool running. The tool returns normally. Use this for showing results — data cards, product grids, status updates.display.pushAndWait({ input }) — push a component and pause the tool until the user responds. The tool's execution is suspended. Use this for collecting input — forms, confirmations, choices.Think of it this way: pushAndForget is like printing a receipt — here's your result. pushAndWait is like handing someone a clipboard — fill this out and give it back.
In glove-react, tools can define their UI inline with a render function. The tool definition and its component live together — no separate files, no string-based lookups.
Here is the weather tool from Getting Started, upgraded with a display card:
import { z } from "zod";
import type { ToolConfig } from "glove-react";
export const weatherTool: ToolConfig = {
name: "get_weather",
description: "Get the current weather for a city.",
inputSchema: z.object({
city: z.string().describe("The city to get weather for"),
}),
// The tool logic — calls your API and pushes a card
async do(input, display) {
const weather = await fetchWeather(input.city);
// Show a weather card — tool keeps running
await display.pushAndForget({ input: weather });
return weather;
},
// The React component that renders the card
render({ data }) {
const { city, temperature, condition } = data as {
city: string;
temperature: string;
condition: string;
};
return (
<div style={{ padding: 16, border: "1px solid #333", borderRadius: 8 }}>
<h3>{city}</h3>
<p>{temperature} — {condition}</p>
</div>
);
},
};When the AI calls get_weather, the do function runs, pushes the weather data onto the display stack, and the render function turns it into a card in your UI.
The useGlove hook exposes slots (the current stack) and renderSlot() (renders a slot using the tool's render function). Add them to your chat component:
"use client";
import { useState } from "react";
import { useGlove } from "glove-react";
export default function Chat() {
const {
timeline,
streamingText,
busy,
sendMessage,
slots, // Active display stack entries
renderSlot, // Renders a slot using its tool's render function
} = useGlove();
const [input, setInput] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || busy) return;
sendMessage(input.trim());
setInput("");
}
return (
<div style={{ maxWidth: 600, margin: "2rem auto" }}>
{/* Conversation timeline */}
<div>
{timeline.map((entry, i) => {
if (entry.kind === "user") return <div key={i}><strong>You:</strong> {entry.text}</div>;
if (entry.kind === "agent_text") return <div key={i}><strong>Assistant:</strong> {entry.text}</div>;
return null;
})}
</div>
{streamingText && <div style={{ opacity: 0.7 }}><strong>Assistant:</strong> {streamingText}</div>}
{/* Display stack — render all active slots */}
{slots.length > 0 && (
<div style={{ margin: "1rem 0", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{slots.map(renderSlot)}
</div>
)}
<form onSubmit={handleSubmit} style={{ display: "flex", gap: "0.5rem" }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask something..."
disabled={busy}
style={{ flex: 1, padding: "0.5rem" }}
/>
<button type="submit" disabled={busy}>Send</button>
</form>
</div>
);
}That's all the wiring you need. Every tool with a render function will now show its UI automatically when the AI calls it.
pushAndWait is the more powerful pattern. It pauses the tool until the user responds. The tool's do function literally awaits the user's answer.
Here is a confirmation tool. The AI calls it before taking a destructive action, and the tool waits for the user to click Confirm or Cancel:
import { z } from "zod";
import type { ToolConfig, SlotRenderProps } from "glove-react";
export const confirmAction: ToolConfig = {
name: "confirm_action",
description:
"Ask the user to confirm before proceeding. " +
"Blocks until the user confirms or cancels.",
inputSchema: z.object({
title: z.string().describe("What you are asking confirmation for"),
message: z.string().describe("Details about the action"),
}),
async do(input, display) {
// This line PAUSES until the user clicks a button
const confirmed = await display.pushAndWait({ input });
// Execution resumes here after the user responds
return confirmed
? "User confirmed the action."
: "User cancelled the action.";
},
render({ data, resolve }: SlotRenderProps) {
const { title, message } = data as { title: string; message: string };
return (
<div style={{ padding: 16, border: "1px dashed #f59e0b", borderRadius: 12 }}>
<p style={{ fontWeight: 600 }}>{title}</p>
<p style={{ color: "#888", marginBottom: 12 }}>{message}</p>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => resolve(true)}>Confirm</button>
<button onClick={() => resolve(false)}>Cancel</button>
</div>
</div>
);
},
};The flow is:
confirm_actiondo function runs and hits display.pushAndWait — the tool pausesrender function shows a dialog with two buttonsresolve(true) is calleddo function resumes with confirmed = true and returns the result to the AIpushAndForget is simpler. It pushes UI onto the stack and the tool keeps running. Use it when you want to show something to the user without pausing.
import { z } from "zod";
import type { ToolConfig, SlotRenderProps } from "glove-react";
export const searchProducts: ToolConfig = {
name: "search_products",
description: "Search the product catalog and display results.",
inputSchema: z.object({
query: z.string().describe("What to search for"),
}),
async do(input, display) {
const results = await catalog.search(input.query);
// Show results — tool does NOT pause
await display.pushAndForget({ input: results });
// Tool returns immediately, AI gets the data too
return results;
},
render({ data }: SlotRenderProps) {
const products = data as { name: string; price: number }[];
return (
<div style={{ display: "grid", gap: 8 }}>
{products.map((p, i) => (
<div key={i} style={{ padding: 12, border: "1px solid #333", borderRadius: 8 }}>
<strong>{p.name}</strong> — ${p.price}
</div>
))}
</div>
);
},
};The product grid appears in the UI while the AI simultaneously receives the raw data and can reference it in its response.
A single tool can use both pushAndForget and pushAndWait. For example, a checkout tool might display a cart summary (fire-and-forget) and then show a payment form (wait for input):
async do(input, display) {
const cart = await getCart(input.cartId);
// Show the cart summary — don't wait
await display.pushAndForget({ input: cart });
// Show a payment form — wait for the user to submit
const paymentDetails = await display.pushAndWait({
input: { total: cart.total },
});
// Both pushes happened, user submitted payment — create the order
return await createOrder(cart, paymentDetails);
},Tools with renderers are registered the same way as plain tools — pass them in the tools array of your GloveClient:
import { GloveClient } from "glove-react";
import { weatherTool } from "./tools/weather";
import { confirmAction } from "./tools/confirm";
import { searchProducts } from "./tools/show-results";
export const gloveClient = new GloveClient({
endpoint: "/api/chat",
systemPrompt: "You are a helpful shopping assistant.",
tools: [weatherTool, confirmAction, searchProducts],
});The framework automatically builds a renderer map from each tool's name and render function. No separate registry step is needed.
The Tool Registry has pre-built tools with renderers that you can copy into your project. Each tool includes the full ToolConfig with do and render already wired together.
For example, to use the confirmation dialog:
tools arrayAvailable tools include confirm_action, collect_form, ask_preference, text_input, show_info_card, suggest_options, and approve_plan.
The render function receives a SlotRenderProps object with two properties:
data — the input that was passed to pushAndWait or pushAndForget. This is how you pass data from the tool's logic to its UI.resolve — a function that resolves the slot. For pushAndWait slots, the value you pass to resolve() becomes the return value in the do function. For pushAndForget slots, calling resolve() removes the slot from the stack.The render function is a regular React component. You can use hooks, state, effects — anything you normally use in React.
| Pattern | Use when | Examples |
|---|---|---|
pushAndForget | You want to show something, tool doesn't need to wait | Search results, data cards, charts, status updates, notifications |
pushAndWait | You need user input before the tool can continue | Confirmations, forms, preference pickers, payment flows, approval dialogs |
| No display | The tool just computes or fetches data | API calls, calculations, database queries that the AI summarizes |
glove-core directly for a REPL-based coding agentpushAndWait and pushAndForgetrender function