Kanzo UI

Discovery

A question over the Guild's archive, answered by a statement anybody can read — an agent with one tool behind Chat, and the rows drawn as a real table rather than a block of JSON.

Open full size

Somebody asks the archive a question in their own words, and gets back the statement that was written for it, the rows it returned, and a sentence quoting numbers it did not choose. The screen is a text-to-SQL panel — the shape every product grows once its data outlives the reports somebody built for it — rebuilt as what it is: an agent with one tool.

It runs without a network, and the split that makes that honest is the same one the Workspace's Ask dock makes: the language is canned, the answer is not. The model is the AI SDK's mock, phrasing five recipes; the statement it writes is executed by the query tool over the running example world, so the table under the answer is a real result set and the prose reads it back. Everything between the two — the agent loop, the tool call, its states, the streaming — is the real ToolLoopAgent and useAgentChat.

Worth pressing in the frame:

  • The five questions above the composer are the ones this agent can answer. Pressing one asks it.
  • Open the thinking above a call: why the agent chose that relation, streamed before it wrote anything.
  • Ask what each member was paid last winter. The agent writes a statement and the call goes red: there is no payments relation, and the tool fails instead of joining two things that both hold money. A refusal is a state of the call, not an error banner over the page.
  • Ask something outside the five. There is no call to make, so the answer carries the refusal on its own.
  • Press Stop mid-answer. The stream is cancelled at the model, not hidden on screen.

The host draws one thing

Chat draws every part of the AI SDK's message as it arrives — text as markdown, reasoning folded away, each tool call in its frame with the SDK's own state. The one thing it cannot know is what a result means, so a host passes a renderer per tool name and it draws inside the frame, in place of its input and output, at every state of the call:

<Chat
  chat={useAgentChat(agent)}
  proposals={RECIPES.map((recipe) => ({ text: recipe.question }))}
  tools={{ query: (part) => (part.state === "output-available" ? <ResultTable {...(part.output as ResultSet)} /> : <ToolInput />) }}
/>

The columns of a result are not known until the statement has been written, so the output is a real DataTableRoot built from the result's own shape. A JSON rendering is what the frame falls back to for a tool with no renderer — never the one thing on offer.

The table comes from the @kanzo-tech/ui/table subpath, never the root barrel — TanStack is an optional peer, and a reader who only wanted a transcript does not pay for it.

The agent is the swap

import { Chat, ToolInput, useAgentChat } from "@kanzo-tech/ai";
import { ToolLoopAgent, jsonSchema, tool } from "ai";

ToolLoopAgent takes a model and the tools; useAgentChat runs it in the page. A product swaps one thing and nothing else on the screen changes: the model becomes localModel, in the page.

  • Chat — the transcript, the composer and the stream, whole. The pin, send/stop/retry and the tool states are its business, not this file's.
  • DataTableRoot / DataTableContent / DataTablePagination / useDataTable — the result set, six rows to a page, columns derived from the projection.
  • ShellRoot / ShellHeader / ShellBody / ShellMain / ShellAside — the frame. The schema rail is the <aside>; the transcript is the single <main>.
  • EmptyRoot, Badge, Separator — the rest.

Where the domain lives

Nothing above knows what a contract is. The archive is the docs' own running example — the Guild's board and its roster — and the two relations the rail lists are those two files described as a schema, row counts included. data.ts holds the five recipes, each with the statement the model writes, the function that computes it, and the sentence that reads the rows back; default.tsx holds the agent and the arrangement.

docs/showcases/discovery/
├── default.tsx      — the shell, the agent and its query tool, Chat
├── data.ts          — the schema rail's relations, the five recipes, and the statement runner
└── result-table.tsx — a result set as the real table, columns built from the projection

The agent refuses a sixth question rather than guessing a join, and that refusal is written in data.ts beside the statement it would have sent. A screen that answers everything is the one nobody can check.