Skip to main content
fxtr ships a web viewer that shows a job as it runs and after it finishes: the graph of workflows and steps, the arrays flowing between them, and the entities they reference. Everything has a default rendering, so a new project gets a usable viewer with no extra work. However, you can customize the visualization by writing renderers, small React components that take over how a particular entity type, step, or workflow is shown, which fxtr loads them into the viewer alongside its own.

Overview

  • Open the viewer with fxtr view to watch an experiment and inspect its results.
  • Define renderers as a TypeScript package inside your project, in its views/ directory, using the fxtr-view library.
  • 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:
This serves the viewer at 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.
The first sentence of each step’s and workflow’s docstring is what appears on its card, so write those as short imperative sentences that tell a reader what the operation does.

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) and unwrap(...) for loading an entity’s data, and EntityLink for linking to another entity.
  • fxtr-view-kit: a set of conveniences for building renderers: layouts, a Kind label, FieldList, message lists for conversations, singleInvocation and eachInvocation for wrapping a view of one invocation into a panel, and LoadEach for loading many entities at once.
A renderer for an entity type is an ordinary component that receives the entity’s ID:
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
Each registration names the slot, the subject, an ID for the renderer, and the component. Prefix renderer IDs with your project’s name: an ID that collides with another renderer for the same slot and subject, including one of the viewer’s own, keeps the whole bundle from loading. Several renderers may target the same slot and subject, in which case the viewer offers a switcher.

Results overviews

The most valuable renderer in most projects is an InvocationPanel 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 as var(--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
The preset builds the entry into a view bundle, a small fxtr-defined format: an ES module holding the renderers, an optional stylesheet, and a manifest naming both and recording the version of the view API they were built against. React, jotai, and 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.