Bee Box documentation · directory: https://beebox.run/docs/reference/ · index: https://beebox.run/docs/reference/index.md · root: https://beebox.run/llms.txt
# Views: Agent-Generated React Components
Views are `.tsx` files in the `src/views/` directory at the box root. They get compiled server-side and rendered in the browser. **A view is always attached to a card type** — it exports `rendersCardTypes` and becomes that type's interface on card pages, in chat embeds, and in the companion pane. There is no card-less "standalone" view.
## When to Create a View
Create a view to give a **card type** a richer interface than the default markdown rendering — an interactive layout, a chart, a structured summary of the card's data, editable controls. A dashboard over many cards is itself a card type (e.g. a `.dashboard.card` whose view reads a collection).
Don't create a view for:
- Simple one-off answers (just reply in chat)
- Static text that doesn't change (use a card or document)
- Something that requires server-side processing (use a job instead)
## File Format
Each view is a `.tsx` file with named exports for metadata and a default export for the component:
```tsx
export const name = "Ledger Overview";
export const description = "Interface for an ledger-overview card";
export const dependencies = ["_content/**/*.ledger-overview.card", "_bookkeeping/archive/**/*.record.card"];
export const modes = ["page", "chat"];
export const rendersCardTypes = ["ledger-overview"];
export default function EstateOverview({ cards, navigate, boxSlug, params, viewHistory }) {
// params.path is the card being displayed; dependencies must cover it.
const card = cards.find(c => c.path === params.path);
const records = cards.filter(c => c.type === "record");
const section = typeof viewHistory.state.section === "string"
? viewHistory.state.section
: "summary";
const openRecords = () => {
const next = { section: "records" };
if (viewHistory.canPush) viewHistory.pushState(next);
else viewHistory.replaceState(next);
};
return (
);
}
```
## Metadata Exports
| Export | Type | Required | Description |
|--------|------|----------|-------------|
| `name` | string | Yes | Human-readable name shown in UI |
| `description` | string | Yes | What this view shows |
| `dependencies` | string[] | Yes | Glob patterns for files that affect rendering |
| `modes` | string[] | Yes | Where the view can appear: `"page"`, `"chat"`, or both |
| `rendersCardTypes` | string[] | No | Card types this view renders — see below |
### Rendering a card type
A view that exports `rendersCardTypes` becomes the **default renderer for
those card types everywhere cards display** — the card page
(`/card/`), chat embeds, and peeks. This is how a custom card type
(e.g. a box-local schema) gets a custom UI without touching the app:
```tsx
export const name = "Sandbox";
export const description = "Interactive sandbox card UI";
export const dependencies = ["**/*.sandbox.card"];
export const modes = ["page", "chat"];
export const rendersCardTypes = ["sandbox"];
export default function Sandbox({ cards, params }) {
const card = cards.find((c) => c.path === params.path);
// params.path is the card being displayed; dependencies must cover the
// type so the card arrives in `cards`.
...
}
```
The built-in renderers (Card, Source) stay available through the
renderer toggle. One view per type: if several views claim the same card
type, the first by slug order wins. Without `rendersCardTypes`, custom
types fall back to the generic built-ins.
## Component Props
The default export receives a `ViewProps` object:
| Prop | Type | Description |
|------|------|-------------|
| `cards` | ViewCard[] | All cards matching the dependency globs |
| `files` | ViewFile[] | Metadata for non-card files matching the globs: `{path, size, mtimeMs}` |
| `readFile` | (path, opts?) => Promise | Fetch a file's text; `{start, end}` byte range, negative start = tail |
| `fileUrl` | (path) => string | Original box file URL — audio, downloads, and full-resolution image links |
| `imageUrl` | (path, options) => string | Cached, bounded image URL; options are `width`/`height`, `fit`, `quality`, `format`, and `dpr` |
| `writeFile` | (path, {content, expect?}) => Promise | Create/overwrite (parents made); returns the new ViewFile; never commits |
| `appendFile` | (path, {content, expect?}) => Promise | Append (creates when missing); same semantics |
| `commitFile` | (path, message) => Promise<{committed, hash?}> | Commit the file + its attachments, nothing else |
| `adapterFetch` | (adapter, {path, ...init}) => Promise | Call an external API with the box's key injected server-side |
| `navigate` | (path: string) => void | Navigate within the box (e.g., `navigate("chat")`) |
| `boxSlug` | string | The current box slug |
| `params` | Record | Query parameters from the URL (e.g., `params.path`) |
| `viewHistory` | { state, canPush, pushState, replaceState } | Explicit JSON-safe navigation state. Read `state`; use `pushState(next)` for a new Back/Forward entry or `replaceState(next)` to normalize the current entry. Check `canPush` when the UI depends on browser history. Do not call `window.history` directly. |
| `reportActivity` | (kind, detail?) => void | When open in the chat companion pane, tell the agent the user touched this card. Writes auto-report `"modified"`; call `reportActivity("explored", detail)` when the user changes the view's *parameters* (filters, ranges, a selected tab) without changing data. The optional `detail` is a short free-text string surfaced to the agent as the `` element's text (e.g. the query the user typed and its top result) — it overwrites any prior detail for the same kind, so calling it on every keystroke is fine. A no-op for inline/page renders, so always safe to call. |
`viewHistory` is opt-in persistence for meaningful view navigation, not a
snapshot of React state. Values must be JSON-safe objects. Validate members
before use because an old/shared URL may contain state from another version of
the view. Unknown or obsolete values should fall back to the view's default;
after a user action, a view may normalize them with `replaceState`. Do not
write history during render. Renderer `params` remain external configuration,
while React `useState` remains transient interaction state.
### ViewCard Structure
Cards are YAML frontmatter + a markdown body. Each card in the `cards` array has:
```typescript
{
path: string; // Box-relative path (e.g., "_bookkeeping/archive/Foo.record.card")
type: string; // Card type, from the filename Foo..card (e.g., "record", "memo")
frontmatter?: Record; // Parsed YAML frontmatter (body and type excluded)
body?: string; // Markdown body
attachments?: ViewFile[]; // Deep listing of the card's attach scope
}
```
Read a card's fields from `frontmatter` (e.g. `card.frontmatter?.title`, the
status from `card.frontmatter?.status`) and its prose from `body`. `type` is
the card type — filter a mixed `cards` array with
`cards.filter(c => c.type === "memo")`. `frontmatter` values are whatever the
card's schema declares (strings, numbers, arrays, nested objects), so they are
typed `unknown` — narrow before use.
`attachments` is how a view discovers what lives next to a card — every file
in the card's attach scope, recursively, as `{path, size, mtimeMs}` with
box-relative paths (e.g.
`_content/playground/Playground.attach/sessions/history.jsonl`). Content is
never inlined — attachments can be huge or binary — fetch it with
`readFile(path)` or point an ``/`