stories
v0.1.3Component stories for any React project — builds CSF story files with the project's own Vite (monorepo-aware), indexes components and their states per git line, compares the working tree with a branch, a commit or a chat turn, renders deterministic screenshots and DOM/React trees through the browser worker, and injects an explorer and compare page into the console.
Install
iii trigger compose::add worker=storiesRun in your project with the iii engine running. Exact versions are immutable and digest-pinned.
Full markdown
/registry/workers/stories.md. Paste it into an LLM prompt or pipe it through curl from a worker.Install
iii trigger compose::add worker=stories@0.1.3Configuration
- data_path: data/stories
keep_lines: 12
viewport:
dpr: 1
height: 768
width: 1024
watch: true
workspaces:
- name: workspace
path: .Dependencies
Readme
stories
Component stories for any React project, rendered in the iii console. The
worker finds CSF story files (*.stories.tsx and friends) in one or more
workspaces, builds them with the project's own Vite (its plugins,
aliases, CSS pipeline and React), and keeps an index per git line: the
working tree, its previous build, any ref or commit, or a chat turn's
before/after tree. Two lines compare into a list of components that changed
directly (the story file or a direct import), indirectly (a deeper input),
were added or removed. Through the browser worker it renders deterministic
screenshots and DOM/React trees of any component state, with args and
globals overrides, and diffs two versions of a state into a verdict a
reviewer can act on. A console page shows the explorer (files as on disk,
git status, impact chips, live preview, props) and the compare view (line A
versus line B, side by side).
No Storybook runtime is needed. Existing .stories.* files and
.storybook/preview.* keep working; storybook/test's fn() is shimmed and
records an actions log. play functions, MDX and addons are ignored.
Install
iii trigger compose::add worker=storiesRequirements on the host: git, Node 22+ with npm on PATH (the compiler
is materialized under data/stories/compiler and installed there once), and
the browser worker for screenshots and trees. Each project must have its
dependencies installed (react and react-dom 18+); a project with its own
vite is built with that version.
Configuration (stories)
| Field | Default | Meaning |
|---|---|---|
data_path |
data/stories |
Lines, content-addressed files, renders, history, the compiler. |
console_url |
unset | Console origin the browser worker navigates to for renders. Unset: the ADE's address from console::status. |
watch |
true |
Rebuild the working tree when a story input changes. |
keep_lines |
12 |
Built ref/turn lines kept per workspace before pruning. |
viewport |
1024×768 @1 |
Default render viewport. |
workspaces[] |
one at . |
{ name, path, projects?, stories?, ignore?, base? } |
A workspace is a repository. Projects inside it are discovered from the story
files (the nearest package.json), so client-app/ and admin-app/ in one
repo each build with their own config and React. Shared packages resolve to
their real paths, so a change in packages/ui impacts stories in every
project that imports it. base is the ref changes are classified against
(default HEAD).
Functions
| Function | What it does |
|---|---|
stories::workspaces / stories::lines |
Git state, projects, component count and built lines per workspace. |
stories::components::list |
Components of a line with their states; filters by project, group, tag, path, query and change kind against a base line. |
stories::components::get |
One component: states with args, argTypes and inferred controls, inputs with hops, change, related components, history. |
stories::tree::fs |
The explorer tree: folders and story files as on disk with git status and change kinds. |
stories::builds::create / stories::builds::get |
Build a line (worktree, a ref, a sha, turn:) and poll it. |
stories::compare |
Every component that differs between two lines, classified, with the changed files. |
stories::diff::file |
Unified diff of one input between two lines. |
stories::screenshot |
Deterministic PNG of one state (args, globals, viewport, clip), cached by content. |
stories::tree |
DOM tree with layout boxes, computed styles and owning component; React tree; accessibility outline; html; actions. |
stories::diff |
One state across two lines: tree changes, pixel regions mapped to elements, verdict identical / invisible / visual / unexplained. |
stories::history |
Versions of one component seen by working-tree builds. |
Triggers: stories:changed fires after a working-tree rebuild with the
classified changes ({ workspace?, project?, kinds? }); stories:build
fires on every build state transition.
The review workflow for agents lives in skills/.
Lines and data
data/stories/<workspace>/
lines/worktree/ the live working tree (rebuilt by the watcher)
lines/worktree.prev/ the build before it
lines/<sha>/ a commit or a turn tree, immutable
files/<sha256> build outputs and story inputs, shared by all lines
renders/<hash>/ shot.png, tree.json, meta.json of one render
history/<project>__<id>.jsonlA ref builds in a detached git worktree; a turn tree comes from the ide
worker's private snapshot repository through git archive. node_modules
folders are linked from the live checkout. A component's version is the
hash of every input's content, so a component unchanged between two lines
shares the same render cache.
Story documents are served to the console through stories::ui-file behind
the console's /ui-files/stories/ route; the page
embeds them in a same-origin iframe.
Development
# terminal 1: rebuild the console page on save
pnpm --dir stories/ui watch
# terminal 2: hot reload the page assets
cd stories && III_STORIES_UI_WATCH=1 cargo runIII_STORIES_NODE / III_STORIES_NPM override the binaries;
III_STORIES_IDE_TURNS points at the ide worker's turn store (default
data/shell/turns). Tests: cargo test; the pipeline test runs against a
real project when III_STORIES_FIXTURE= and
III_STORIES_COMPILER_DIR= are set.
Limits
React 18+ only. Projects built by webpack or Next.js need a vite.config
with the mocks they rely on (next/link, next/image). Stories that reach
application singletons (routers, engine clients) render with errors and show
up with an error field. Pixel diffs compare renders from one machine; a
different font stack between hosts reads as unexplained.
Skills
API reference (JSON)
{
"functions": [
{
"description": "Internal: re-read the stories configuration after an operator change.",
"metadata": {
"internal": true
},
"name": "stories::on-config-change",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Trigger payload for `<worker>::on-config-change`. Advisory only: handlers re-fetch the authoritative value and ignore it, so a direct call can never inject config.",
"properties": {
"id": {
"default": null,
"description": "Configuration id that changed.",
"type": [
"string",
"null"
]
}
},
"title": "OnConfigChangeEvent",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Ack returned by the internal `<worker>::on-config-change` handler.",
"properties": {
"ok": {
"type": "boolean"
}
},
"required": [
"ok"
],
"title": "OnConfigChangeResponse",
"type": "object"
}
}
],
"triggers": [
{
"description": "Fires on every build state transition (queued, running, done, failed) of any line. Config: { workspace?, metadata? }.",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"metadata": true,
"workspace": {
"type": [
"string",
"null"
]
}
},
"title": "BuildConfig",
"type": "object"
},
"metadata": {},
"name": "stories:build",
"return_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"at": {
"type": "string"
},
"build_id": {
"type": "string"
},
"line": {
"type": "string"
},
"message": {
"type": [
"string",
"null"
]
},
"status": {
"description": "queued | running | done | failed",
"type": "string"
},
"workspace": {
"type": "string"
}
},
"required": [
"at",
"build_id",
"line",
"status",
"workspace"
],
"title": "BuildEvent",
"type": "object"
}
},
{
"description": "Fires after a working-tree rebuild changed components, with each change classified as direct, indirect, new or removed. Config: { workspace?, project?, kinds?, metadata? }.",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kinds": {
"description": "Change kinds to fire for: direct, indirect, new, removed. Omit for all.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"metadata": {
"description": "Metadata attached to every invocation this binding receives."
},
"project": {
"description": "Project name. Omit for every project.",
"type": [
"string",
"null"
]
},
"workspace": {
"description": "Workspace slug. Omit for every workspace.",
"type": [
"string",
"null"
]
}
},
"title": "ChangedConfig",
"type": "object"
},
"metadata": {},
"name": "stories:changed",
"return_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Change": {
"properties": {
"a_version": {
"type": [
"string",
"null"
]
},
"b_version": {
"type": [
"string",
"null"
]
},
"file": {
"type": "string"
},
"files": {
"default": [],
"items": {
"$ref": "#/definitions/ChangedFile"
},
"type": "array"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/ChangeKind"
},
"project": {
"type": "string"
},
"states": {
"default": [],
"items": {
"$ref": "#/definitions/StateChange"
},
"type": "array"
},
"title": {
"type": "string"
}
},
"required": [
"file",
"id",
"kind",
"project",
"title"
],
"type": "object"
},
"ChangeKind": {
"oneOf": [
{
"enum": [
"unchanged",
"new",
"removed"
],
"type": "string"
},
{
"description": "The story file or one of its direct imports changed.",
"enum": [
"direct"
],
"type": "string"
},
{
"description": "Only deeper inputs changed.",
"enum": [
"indirect"
],
"type": "string"
}
]
},
"ChangeSummary": {
"properties": {
"direct": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"indirect": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"new": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"removed": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"unchanged": {
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"direct",
"indirect",
"new",
"removed",
"unchanged"
],
"type": "object"
},
"ChangedFile": {
"properties": {
"hop": {
"description": "Smallest hop at which the file reaches the component.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
},
"status": {
"description": "added | removed | modified",
"type": "string"
}
},
"required": [
"hop",
"path",
"status"
],
"type": "object"
},
"LineInfo": {
"properties": {
"built_at": {
"description": "RFC 3339.",
"type": "string"
},
"dirty": {
"description": "Whether the working tree had uncommitted changes when built.",
"type": [
"boolean",
"null"
]
},
"key": {
"description": "Stable key: `worktree`, `worktree.prev`, or a git object sha.",
"type": "string"
},
"kind": {
"$ref": "#/definitions/LineKind"
},
"label": {
"description": "Human label: the ref name, the turn, or `working tree`.",
"type": "string"
},
"sha": {
"type": [
"string",
"null"
]
}
},
"required": [
"built_at",
"key",
"kind",
"label"
],
"type": "object"
},
"LineKind": {
"description": "Which kind of source tree a line photographs.",
"oneOf": [
{
"description": "The live working tree, rebuilt by the watcher. Key `worktree`.",
"enum": [
"worktree"
],
"type": "string"
},
{
"description": "The working tree as it was before the last rebuild. Key `worktree.prev`.",
"enum": [
"prev"
],
"type": "string"
},
{
"description": "A commit reached through a ref or a sha. Key: the commit sha.",
"enum": [
"ref"
],
"type": "string"
},
{
"description": "A chat turn's before/after tree from the ide worker. Key: the tree sha.",
"enum": [
"turn"
],
"type": "string"
}
]
},
"StateChange": {
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"description": "added | removed | kept",
"type": "string"
}
},
"required": [
"id",
"name",
"status"
],
"type": "object"
}
},
"properties": {
"at": {
"type": "string"
},
"base": {
"allOf": [
{
"$ref": "#/definitions/LineInfo"
}
],
"description": "The line the changes were classified against."
},
"changes": {
"description": "Only the changed components (never `unchanged`).",
"items": {
"$ref": "#/definitions/Change"
},
"type": "array"
},
"line": {
"$ref": "#/definitions/LineInfo"
},
"summary": {
"$ref": "#/definitions/ChangeSummary"
},
"workspace": {
"type": "string"
}
},
"required": [
"at",
"base",
"changes",
"line",
"summary",
"workspace"
],
"title": "ChangedEvent",
"type": "object"
}
}
]
}