DataBrowser is the registry block for row-oriented AI answers. It covers two related tools:
show_tablerenders a static table snapshot when the rows are already available.show_data_browserrenders a richer browsing surface for search, filters, sorting, pagination, row detail, and row actions.
Both tools live on this page because they share the same data model, table rendering internals, and safety boundary. The difference is intent: ShowTable answers with a bounded snapshot, while DataBrowser lets the user inspect a resource or a larger local row set.
| Customer | Order # | Status | Region | Total |
|---|---|---|---|---|
| 1001 | paid | Cairo | 75 | |
| 1002 | new | Giza | 112 | |
| 1003 | review | Alexandria | 149 | |
| 1004 | canceled | Mansoura | 186 | |
| 1005 | paid | Cairo | 223 | |
| 1006 | new | Giza | 260 |
import { DataBrowser } from "@/components/fable-ui/data-browser/data-browser"
const columns = [
{ key: "customer", label: "Customer", sortable: true },
{ key: "orderNumber", label: "Order #", width: 132 },
{ key: "status", label: "Status", type: "badge", filterable: true },
{ key: "region", label: "Region", filterable: true },
{ key: "total", label: "Total", type: "currency", align: "right" },
]
Installation#
pnpm dlx shadcn@latest add shobky/fable-ui/data-browserThe data-browser registry item installs the browser surface, the table surface, shared hooks, internal table pieces, the tool definitions, and manifests:
components/fable-ui/data-browser/index.ts
lib/fable-ui/tools/show-data-browser-tool.ts
lib/fable-ui/tools/show-table-tool.ts
components/fable-ui/data-browser/data-browser.tsx
components/fable-ui/data-browser/data-browser.types.ts
components/fable-ui/data-browser/show-table.tsx
components/fable-ui/data-browser/resolve-intent.ts
components/fable-ui/data-browser/hooks/use-data-browser-query.ts
components/fable-ui/data-browser/hooks/use-data-browser-actions.ts
components/fable-ui/data-browser/hooks/use-row-detail-surface.ts
components/fable-ui/data-browser/internal/inline-error.tsx
components/fable-ui/data-browser/internal/pagination-footer.tsx
components/fable-ui/data-browser/internal/responsive-detail-surface.tsx
components/fable-ui/data-browser/internal/row-detail.tsx
components/fable-ui/data-browser/internal/table-view.tsx
components/fable-ui/data-browser/internal/toolbar.tsx
components/fable-ui/data-browser/lib/cell-avatar.tsx
components/fable-ui/data-browser/lib/fallback-filters.ts
components/fable-ui/data-browser/lib/get-row-id.ts
lib/fable-ui/manifests/show-table.md
lib/fable-ui/manifests/show-data-browser.mdThe component depends on ai, zod, class-variance-authority, lucide-react, shared Fable core, and shadcn UI primitives such as card, button, badge, input, dialog, drawer, and select.
Install a driver only when show_data_browser should read real host-owned data from your backend. A driver belongs in your app, not in the model prompt. It fetches, authorizes, validates, and returns rows for an allowlisted resource.
pnpm dlx shadcn@latest add shobky/fable-ui/rest-driverpnpm dlx shadcn@latest add shobky/fable-ui/firebase-driverData flow#
For static data, the model can call show_table or show_data_browser with safe columns and rows that are already present in the conversation.
For real backend data, the model should call show_data_browser with a resourceId from the host-provided resource manifest. The host app resolves that resource id through the registered data source, checks permissions, applies allowed filters and sorts, and sends safe rows back to the component. The model must not receive raw SQL, Firestore collection paths, REST endpoints, secrets, or authorization rules.
flowchart LR
User["User asks for orders"] --> AI["AI selects show_data_browser"]
AI --> Tool["Tool input contains resourceId"]
Tool --> Host["Host app resolves registered resource"]
Host --> Backend["Backend or driver fetches authorized rows"]
Backend --> Renderer["Fable renderer validates payload"]
Renderer --> UI["DataBrowser renders rows and states"]Usage#
This example is paste-ready. It includes dummy columns and rows so the component works out of the box.
import { DataBrowser } from "@/components/fable-ui/data-browser/data-browser"
const columns = [
{ key: "customer", label: "Customer", sortable: true },
{ key: "orderNumber", label: "Order #", width: 132 },
{ key: "status", label: "Status", type: "badge", filterable: true },
{ key: "region", label: "Region", filterable: true },
{ key: "total", label: "Total", type: "currency", align: "right" },
]
const rows = [
{
id: "ord_1001",
customer: "Nadia Ali",
orderNumber: "1001",
status: "paid",
region: "Cairo",
total: 420,
},
{
id: "ord_1002",
customer: "Mina Fahmy",
orderNumber: "1002",
status: "review",
region: "Giza",
total: 185,
},
{
id: "ord_1003",
customer: "Sarah Adel",
orderNumber: "1003",
status: "new",
region: "Alexandria",
total: 268,
},
{
id: "ord_1004",
customer: "Omar Saleh",
orderNumber: "1004",
status: "paid",
region: "Mansoura",
total: 512,
},
]
export function OrdersBrowser() {
return (
<DataBrowser
title="Orders"
entityLabel="orders"
columns={columns}
rows={rows}
pageSize={6}
searchPlaceholder="Search orders"
detail={{
title: "Order details",
description: "Review the selected row.",
fields: ["customer", "orderNumber", "status", "region", "total"],
}}
/>
)
}Row Detail#
When detail, renderDetail, onViewRow, or row actions are present, the table includes a View column. By default, View opens a dialog on desktop and a drawer on mobile. The default surface renders a key/value grid from visible columns, narrowed by detail.fields when provided, and includes row action controls when actions exist.
<DataBrowser
title="Orders"
entityLabel="orders"
columns={columns}
rows={rows}
detail={{
title: "Order details",
description: "Selected order fields.",
fields: ["customer", "orderNumber", "status", "total"],
}}
/>Use renderDetail(row) when you want custom content inside Fable's default dialog/drawer. Use onViewRow(row) when the host app should own the whole destination, such as a route, side panel, modal, or product-specific drawer. When onViewRow is provided, Fable calls the host handler and does not open the default detail surface.
<DataBrowser
title="Orders"
entityLabel="orders"
columns={columns}
rows={rows}
onViewRow={(row) => router.push(`/orders/${row.id}`)}
/>ShowTable#
show_table is for static snapshots where rows are already available. It supports up to 200 rows with predictable pagination.
| Customer | Order # | Status | Region | Total |
|---|---|---|---|---|
| 1001 | paid | Cairo | 75 | |
| 1002 | new | Giza | 112 | |
| 1003 | review | Alexandria | 149 | |
| 1004 | canceled | Mansoura | 186 | |
| 1005 | paid | Cairo | 223 | |
| 1006 | new | Giza | 260 |
import { ShowTable } from "@/components/fable-ui/data-browser/show-table"
export function RecentOrdersTable() {
return (
<ShowTable
title="Recent orders"
description="A paginated static snapshot."
columns={columns}
rows={rows}
pageSize={6}Use it when an answer needs a compact list such as "recent orders", "top customers", "failed jobs", or "matching invoices". It should not be used when the user needs to browse a live resource, change filters repeatedly, inspect row detail, or trigger row actions.
import { ShowTable } from "@/components/fable-ui/data-browser/show-table"
const columns = [
{ key: "customer", label: "Customer" },
{ key: "status", label: "Status", type: "badge" },
{ key: "total", label: "Total", type: "currency", align: "right" },
]
const rows = [
{ id: "ord_1001", customer: "Nadia Ali", status: "paid", total: 420 },
{ id: "ord_1002", customer: "Mina Fahmy", status: "review", total: 185 },
]
export function RecentOrdersTable() {
return (
<ShowTable
title="Recent orders"
description="A paginated static snapshot."
columns={columns}
rows={rows}
pageSize={6}
/>
)
}Tools#
show_data_browser is for browsing, search, filter, sort, pagination, or row detail over host-owned data.
The model must not pass raw SQL, raw Firestore query code, secrets, or authorization decisions. The host supplies data access, allowed filters, allowed sort fields, permissions, and row actions.
Rows with avatar-like fields such as avatar, avatarUrl, image, imageUrl, picture, pictureUrl, photo, or photoUrl render a compact visual avatar in the first column. If no image URL is available, the table can fall back to initials from a name or title field.
Tool definition#
lib/fable-ui/tools/show-data-browser-tool.ts exports both tools. The schemas describe safe column and row payloads, the AI SDK tool objects describe how the model can call them, and the renderer entries map validated tool parts to <ShowTable /> or <DataBrowser />. lib/fable-ui/tools/show-table-tool.ts re-exports the table-only contract for a consistent tool import path.
import { lazy } from "react"
import { tool } from "ai"
import { z } from "zod"
import {
defineFableComponent,
fableRegistry,
type DataSourceRegistry,
} from "@/lib/fable-ui/core"
import {
dataColumnSchema,
dataFilterSchema,
dataSortSchema,
sortStateSchema,
} from "@/lib/fable-ui/core/schemas"
import { resolveDataBrowserIntent } from "@/components/fable-ui/data-browser/resolve-intent"
const DataBrowser = lazy(() =>
import("@/components/fable-ui/data-browser/data-browser").then((module) => ({
default: module.DataBrowser,
})),
)
const ShowTable = lazy(() =>
import("@/components/fable-ui/data-browser/show-table").then((module) => ({
default: module.ShowTable,
})),
)
const rowSchema = z.record(z.string(), z.unknown())
export const showTableInputSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
columns: z.array(dataColumnSchema).min(1).max(12),
rows: z.array(rowSchema).max(200),
pageSize: z.number().int().positive().max(100).optional(),
})
export type ShowTableInput = z.infer<typeof showTableInputSchema>
export const showDataBrowserInputSchema = z.object({
title: z.string().min(1),
entityLabel: z.string().min(1).optional(),
description: z.string().optional(),
resourceId: z
.string()
.min(1)
.optional()
.describe(
"Use only a resource id from fableRegistry.getAgentResourceManifest(). Never invent resource ids."
),
searchPlaceholder: z.string().optional(),
columns: z.array(dataColumnSchema).optional(),
rows: z.array(rowSchema).max(200).optional(),
filters: z.array(dataFilterSchema).optional(),
sortOptions: z.array(dataSortSchema).optional(),
initialFilters: z.record(z.string(), z.unknown()).optional(),
initialSort: sortStateSchema.optional(),
initialSearch: z.string().optional(),
visibleColumns: z.array(z.string().min(1)).optional(),
detail: z
.object({
title: z.string().optional(),
description: z.string().optional(),
fields: z.array(z.string()).optional(),
})
.optional(),
pageSize: z.number().int().positive().max(100).optional(),
})
export type ShowDataBrowserInput = z.infer<typeof showDataBrowserInputSchema>
export function describeAvailableResources(
registry: DataSourceRegistry = fableRegistry
) {
return JSON.stringify(registry.getAgentResourceManifest(), null, 2)
}
export function createShowTableTool() {
return tool({
description:
"Show a static table snapshot when display-ready rows are already available. Supports up to 200 rows with pagination. Do not use for raw SQL, raw Firestore paths, secrets, or authorization decisions.",
inputSchema: showTableInputSchema,
execute: async (input) => input,
})
}
export const showTableTool = createShowTableTool()
export function createShowDataBrowserTool() {
return tool({
description: [
"Show a trusted data browser for search, filters, sort, pagination, or row detail.",
"Use resourceId only from fableRegistry.getAgentResourceManifest(); never invent resource IDs.",
"Never pass raw SQL, raw Firestore paths, raw collection names, secrets, or authorization decisions.",
"The host owns data access, permissions, validation, allowed filters, allowed sorts, and row actions.",
"If no resourceId is available, include safe static rows and columns only when the data is already present.",
].join(" "),
inputSchema: showDataBrowserInputSchema,
execute: async (input) => input,
})
}
export const showDataBrowserTool = createShowDataBrowserTool()
export const showTable = defineFableComponent({
name: "show_table",
schema: showTableInputSchema,
tool: showTableTool,
renderer: {
Component: ShowTable,
loadingProps: { title: "Table", columns: [], rows: [], isLoading: true },
emptyProps: { title: "Table", columns: [], rows: [] },
errorProps: (description: string) => ({
title: "Table unavailable",
columns: [],
rows: [],
error: { title: "Table unavailable", description },
}),
toProps: (data: ShowTableInput) => ({ ...data }),
},
})
export const showDataBrowser = defineFableComponent({
name: "show_data_browser",
schema: showDataBrowserInputSchema,
tool: showDataBrowserTool,
renderer: {
Component: DataBrowser,
loadingProps: {
title: "Data browser",
entityLabel: "rows",
columns: [],
rows: [],
isLoading: true,
},
emptyProps: {
title: "Data browser",
entityLabel: "rows",
columns: [],
rows: [],
},
errorProps: (description: string) => ({
title: "Data browser unavailable",
entityLabel: "rows",
columns: [],
rows: [],
error: { title: "Data browser unavailable", description },
}),
toProps: (data: ShowDataBrowserInput) => resolveDataBrowserIntent(data),
},
})
States#
The previews above include the same states the tool renderer can produce:
| State | How it renders |
|---|---|
| Ready | Validated rows, columns, filters, sort, and detail settings render normally. |
| Loading | Skeleton table UI renders while a tool part streams or the host marks the part as loading. |
| Empty | The table renders an empty state when no rows are available. |
| Error | The component receives an error object when the tool errors or schema validation fails. |
| Disabled | Controls render locked when the host marks the part as disabled. |
Manifests#
Manifests are copyable routing contracts. They tell the model when to pick a component, when to avoid it, and how to choose between neighboring tools. Read the Manifests guide for the full convention.
show_table#
---
tool: show_table
type: registry:block
---
# show_table
Use for static snapshots where display-ready rows are already available. Supports up to 200 rows with pagination; set an explicit page size for larger payloads.
Avoid `show_table` when the user needs browsing, filtering, sorting, details, row actions, or live data access. Use `show_data_browser` for those cases.
show_data_browser#
---
tool: show_data_browser
type: registry:block
---
# show_data_browser
Use for browsing, searching, filtering, sorting, pagination, row detail, or row actions over host-owned data.
Static rows are allowed only when display-ready data is already present. Keep static payloads to 200 rows or fewer and set an explicit page size for large payloads.
The model must not pass raw SQL, raw Firestore query code, secrets, or authorization decisions. The host owns data access and allowed operations.
Choosing between them#
Use show_table when the rows are already available in the tool payload. Use show_data_browser when the model should select a registered host-owned resource by resourceId or when the user needs search, filters, sort, pagination, row detail, or row actions.