The Display Stack

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.

How it works

Every tool's do function receives two arguments: the validated input and a display object. The display object has two methods:

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.

Adding a renderer to a tool

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:

lib/tools/weather.tsxtsx
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.

Rendering slots in your app

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:

app/page.tsxtsx
"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 — collecting user input

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:

lib/tools/confirm.tsxtsx
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:

  1. The AI decides an action needs confirmation and calls confirm_action
  2. The do function runs and hits display.pushAndWait — the tool pauses
  3. The render function shows a dialog with two buttons
  4. The user clicks Confirm — resolve(true) is called
  5. The do function resumes with confirmed = true and returns the result to the AI
  6. The AI reads the result and continues (e.g., executes the action)

pushAndForget — displaying results

pushAndForget 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.

lib/tools/show-results.tsxtsx
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.

Combining both patterns

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):

lib/tools/checkout.tsxtsx
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);
},

Registering your tools

Tools with renderers are registered the same way as plain tools — pass them in the tools array of your GloveClient:

lib/glove.tstypescript
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.

Using pre-built tools from the registry

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:

  1. Go to confirm_action in the registry
  2. Copy the source code into your project
  3. Import it and add it to your tools array

Available tools include confirm_action, collect_form, ask_preference, text_input, show_info_card, suggest_options, and approve_plan.

The render function in detail

The render function receives a SlotRenderProps object with two properties:

The render function is a regular React component. You can use hooks, state, effects — anything you normally use in React.

When to use which pattern

PatternUse whenExamples
pushAndForgetYou want to show something, tool doesn't need to waitSearch results, data cards, charts, status updates, notifications
pushAndWaitYou need user input before the tool can continueConfirmations, forms, preference pickers, payment flows, approval dialogs
No displayThe tool just computes or fetches dataAPI calls, calculations, database queries that the AI summarizes

Next steps