Overview
- Open the viewer with
fxtr viewto watch an experiment and inspect its results. - Define renderers as a TypeScript package inside your project, in its
views/directory, using thefxtr-viewlibrary. - Build the package into a view bundle and the viewer picks it up, so your data and your steps appear the way you designed.
The experiment viewer
From inside your project:http://127.0.0.1:8000/ and opens it in your browser. Every launcher
prints a link of the form http://127.0.0.1:8000/#job=<job id> that opens its job directly, and
you can switch between the project’s jobs from a menu in the viewer. A running job updates live.
For one job, the viewer shows:
- The tree of workflow invocations. The root workflow at the top, with each child workflow one level down. A mapped child appears once per key.
- A card for every step and child workflow a workflow scheduled, showing its docstring, how it was mapped, its progress, and its log lines. A card links to the function’s code in your editor.
- The arrays each operation received and produced, as tables you can page through and slice by dimension. A value that references an entity is a link.
- Entity details, opened from any link, in a pane you can keep alongside the trace. Hovering a link shows a preview.
- Your project’s README, linked from the side rail.
Customizing visualizations
The viewer decides how to draw each thing by looking up a renderer for it. A renderer is registered for a slot, which is a place in the viewer where something is drawn, and a subject, which says what kind of thing it draws there:
For entities, the subject is the
fxtr_type string from the entity’s definition, which is why
every entity type needs one. For invocations, it is the name= given to @step or @workflow (or, by default, the Python function’s module-qualified name).
If no renderer is registered for a subject, the viewer falls back to generic ones that can be used with any value.
Renderers are implemented using two libraries:
fxtr-view: the small, stable contract between a renderer and the viewer: the slot declarations,renderer(...)for registering a component,useEntity(id)andunwrap(...)for loading an entity’s data, andEntityLinkfor linking to another entity.fxtr-view-kit: a set of conveniences for building renderers: layouts, aKindlabel,FieldList, message lists for conversations,singleInvocationandeachInvocationfor wrapping a view of one invocation into a panel, andLoadEachfor loading many entities at once.
views/src/answer.tsx
unwrap(useEntity(id)) returns the entity’s decoded data, or suspends until it arrives, so the
component body can assume the data is there and leave the loading state to the viewer. The data is
the entity’s stored form: a map with the entity’s fields, where references to other entities arrive
as EntityIDs that EntityLink turns into links.
The project’s bundle collects and exports its renderers:
views/src/bundle.ts
Results overviews
The most valuable renderer in most projects is anInvocationPanel for the root workflow. The
viewer offers it as a full-pane view of the whole run, in place of the trace, and it is where you
present the experiment’s results: the charts, tables, and comparisons someone would want to see
first. It usually reads a report entity that the root workflow returns, which references every
result array worth presenting, so one load gives the overview everything it needs.
A good overview shows what matters most, which may come from several steps rather than only the
last. Where there is room, show distributions rather than only averages. Give charts of the same
kind the same axes. And make anything that corresponds to an entity, such as a verdict or a
conversation, a link to it, so a reader can go from a summary to the evidence behind it in one
click.
Styling
Write every color as one of the viewer’s design tokens, such asvar(--fg), var(--bg),
var(--line), or var(--select-accent), never as a literal like #fff. The viewer has a light
and a dark theme, and the tokens switch with it, while a literal stays the same in both and can end
up unreadable. For the data in a chart, the chart colors var(--chart-1) to var(--chart-8)
and the kit’s sequentialFill and divergingFill are a default palette that stays readable for
color-blind readers in both themes. Styles local to your package go in CSS Modules
(*.module.css) next to the components that use them. The viewer’s look in full, and how to
build charts and controls that fit it, is the view-design.md guide of the fxtr Claude Code
plugin.
Building and loading custom renderers
A project’s renderers live in a TypeScript package of their own,views/ in a project created
by fxtr new, named in pyproject.toml under [tool.fxtr.views]. It is an ordinary npm package
that depends on fxtr-view, fxtr-view-kit, and the fxtr-view-bundle build preset. Those
packages are not on npm: fxtr carries them, and the project keeps its own copy in
views/vendor/fxtr/, which fxtr views vendor refreshes after a fxtr upgrade. What makes it a
fxtr renderer package is how it is built. The package is built with Vite, and its whole
Vite configuration is one call to the preset, naming the bundle’s entry file:
views/vite.config.ts
fxtr-view are not bundled
in; the viewer hands its own copies to the bundle when it loads it, so renderers share the
viewer’s hook state and context. Everything else the renderers import, the kit, a chart library,
your own components, is bundled in.
The viewer loads the project’s bundle when its page loads, checks that the bundle’s view API
version matches its own, and adds the bundle’s renderers after its defaults. Nothing about a
project is compiled into the viewer itself, so one viewer serves every project, and the bundle is
the only thing that changes between them. fxtr views build produces the bundle, and fxtr view
rebuilds it on startup when the renderer sources have changed. The steps, from installing the
package’s dependencies to what to do when the viewer reports a missing or stale bundle, are in
Reporting and viewing results.