# Studio
> A design studio that lives in your repo: studies, engineering notes, and working UI experiments kept beside the code they describe, open to the agents that help you build it.
---
# Studio
**A design studio that lives in your repo.**
Studies, engineering notes, and working UI experiments, kept beside the code they describe, and open to the agents that help you build it.
## Why
Design work tends to drift away from the product: into Figma files, slide decks, and
threads nobody can find a month later. Studio keeps it in the repository. A study is a
React component, a doc is Markdown, and both sit next to the source they argue about.
They're reviewed in pull requests and rendered with the product's real fonts, tokens,
and components.
## What you get
- **A shell for every product.** Sidebar, page strip, focus mode, a registry of pages
grouped by your own taxonomy. Built on [Hudson](https://github.com/hudsonkit/hudson).
- **Docs and code, side by side.** A Markdown renderer for engineering docs and a
read-only CodeMirror viewer, wired to link straight into source files.
- **One URL per project.** `studio dev` gives each repo a stable local host like
`myapp.studio.local`, so you stop juggling ports.
- **Agents as collaborators.** A local MCP server lets coding agents publish pages
into your studio and wait for your feedback on them.
- **Flows.** A spatial canvas for whole product journeys, with each screen backed by a
live design surface.
- **Shareable studies.** Any study becomes a private Claude artifact that reviewers can
comment on without cloning your repo. Their comments come back into Studio.
## Review studies as Claude artifacts
Most people you want feedback from don't have your repo running. Studio bundles a study
into one self-contained HTML page with React, the compiled CSS and the fonts inline.
Your agent publishes that page as a private [Claude artifact](https://claude.ai), and
you share the link like a document.
```txt
build → publish → reviewers comment → import into Studio → reply → mirror back
```
Comments on the artifact come back as threaded feedback on the study. Replies you
write in Studio are posted to the same artifact thread. The Studio host never calls
claude.ai: it keeps the bookkeeping and hands your agent a plan through four MCP tools,
and the agent publishes with its own Artifact tools.
```bash
bun src/artifacts/cli.ts build --all # one HTML page per shared study
studio mcp # connect your agent, then ask it to sync artifacts
```
See [Share studies as Claude artifacts](docs/claude-artifacts.md), or the
[live walkthrough](https://hudsonkit.com/studio/foundations/claude-artifacts).
## Quick start
```bash
git clone https://github.com/hudsonkit/studio
cd studio
bun install
bun dev
```
Open [localhost:5191/studio](http://localhost:5191/studio). `apps/studio` is the
first-party studio, and it's built with this package.
> [!NOTE]
> Studio is early and under active development. Today it's developed side by side with
> [hudsonkit](https://github.com/hudsonkit/hudson). Clone `hudson` next to `studio`
> before installing, since the workspace links it from `../hudson`.
## Adding a studio to your product
A studio is an ordinary app route plus a registry of pages:
```txt
app/studio/[[...slug]]/page.tsx route entry
src/studio/studioRegistry.ts your sections, statuses, and pages
src/studio/StudioPages.tsx the studies themselves
docs/ source docs stay where they are
```
```ts
import { createRegistry } from "studio/registry";
type Bucket = "foundations" | "studies";
type Surface = "web" | "mobile";
type Status = "draft" | "preview" | "stable";
export const registry = createRegistry({
pages: [
{
href: "/studio/studies/onboarding-v2",
label: "Onboarding, take two",
bucket: "studies",
surface: "web",
status: "preview",
source: ["src/studio/studies/OnboardingV2.tsx"],
},
],
surfaceOrder: ["web", "mobile"],
defaultSurface: "web",
bucketLabel: (bucket) => ({ foundations: "Foundations", studies: "Studies" })[bucket],
surfaceLabel: (surface) => surface,
});
```
Then wrap your dev script so the project gets its own host:
```json
{ "scripts": { "dev": "studio dev --port 3060 -- next dev --port 3060" } }
```
## Packages
| Import | What it's for |
| --- | --- |
| `studio/registry` | Typed page registry for your taxonomy |
| `studio/shell` · `studio/app-shell` | Studio layout, and the Hudson app-shell adapter |
| `studio/doc` · `studio/code` | Markdown docs and the CodeMirror viewer |
| `studio/flows` | Journey canvas, model, renderer, and server |
| `studio/feedback` · `studio/agents` | Reviewer feedback and agent dispatch |
| `studio/injection` | Swap a registered study into the live product |
| `studio/theme` · `studio/atoms` · `studio/router` | Tokens, status pills, router adapters |
## Learn more
- [Docs site](https://hudsonkit.com/studio/docs/): every guide and reference, built with [Dewey](https://github.com/arach/dewey)
- [Claude artifacts](docs/claude-artifacts.md): publish studies for review and sync comments both ways
- [Reference](docs/reference.md): every subpath, the local edge, the host API, and conventions
- [Cloud studios](docs/cloud-studios.md): one host on a VM serving many studios, with an agent working in place
- [Agent dispatch](docs/agent-dispatch.md): routing annotations to coding agents
- [Component registry](docs/component-registry.md): audit and verify gates
- [AGENTS.md](https://github.com/hudsonkit/studio/blob/main/AGENTS.md): the working guide for coding agents in this repo
## Development
```bash
bun test # full suite
bun run typecheck
```
## License
[Apache-2.0](https://github.com/hudsonkit/studio/blob/main/LICENSE)
---
# Agent dispatch
`studio/agents` lets a Studio register the agents it may send work to and
route messages — annotations, context, review passes — to a chosen agent over
the existing Scout web transport. It is the multi-agent sibling of the Scout
connection (`studio/scout`): the `scout` block in `.studio/project.json`
says *who this Studio is*; the `agents` list says *who this Studio can talk
to*.
The dispatch contract (`StudioCapabilityRequest` / `StudioCapabilityResult`)
mirrors Hudson's HUD-011 agent contract. Transport today is direct Scout
HTTP; the shapes are aligned so a future Hudson-mediated dispatch bus can
slot in without rewriting call sites.
## Registering targets
Add an `agents` array to `.studio/project.json`, beside the `scout` block:
```json
{
"scout": {
"webBaseUrl": "http://127.0.0.1:43120",
"identity": { "agent": "studio", "label": "Studio agent" }
},
"agents": [
{ "agent": "studio", "label": "Studio agent" },
{ "agent": "atelier", "label": "Atelier agent", "intent": "request" }
]
}
```
Each entry is a `StudioAgentTarget`:
- `agent` (required) — a portable Scout selector, resolved against exact ids,
definition ids, handles, and names. Never a machine-specific id, so the
manifest survives moving between machines.
- `label` — human-facing name for pickers; falls back to the selector.
- `intent` — `"message"` (default) or `"request"`, the send mode this target
prefers.
- `blurb` — one line on what this agent is good at.
Registration is an explicit array, not a discovery sweep: entering the list
is the claim that an agent may receive work from this Studio, and that should
be a decision. When `agents` is absent, every consumer falls back to
`[scout.identity]` — single-agent configs keep working unchanged.
## The registry
```ts
import { createAgentRegistry } from "studio/agents";
const registry = createAgentRegistry({ agents: manifest.agents ?? [] });
registry.all(); // every target, in manifest order
registry.get("atelier"); // one target, or undefined
registry.resolve(undefined); // the default (first, or `defaultTarget`)
registry.resolve("nobody"); // undefined — never silently the default
```
`resolve` is the picker path: no choice falls back to the default, an unknown
selector refuses. Sending work to the wrong agent is worse than not sending.
Server code can skip the manifest plumbing with
`loadStudioAgentRegistry(startDirectory)` from `studio/scout/server`, which
reads `.studio/project.json` and applies the `[scout.identity]` fallback.
## Sending to a target
`postStudioScoutMessage` (server-only, `studio/scout/server`) accepts an
optional `target` on its input — or as a trailing argument — and resolves the
target's selector instead of the hardwired identity:
```ts
await postStudioScoutMessage({
body: "Port this component.",
target: { agent: "atelier", label: "Atelier agent" },
});
```
The receipt carries both `agentId` (the Studio's own resolved identity — the
sender) and `targetAgentId` (the resolved recipient). Without a target they
are equal, and existing callers behave exactly as before.
The first-party app exposes the same flow over HTTP: `GET /api/scout/agents`
lists the registered targets with live online state (Scout down →
`online: null`, never a 500), and `POST /api/scout/messages` accepts an
optional `target` that is validated against the registry — unregistered
selectors get a 400.
## The dispatch contract
Two pure helpers map Studio shapes onto the HUD-011-aligned contract, no
network involved:
```ts
import {
annotationPassToCapabilityRequest,
receiptToCapabilityResult,
} from "studio/agents";
const request = annotationPassToCapabilityRequest(pass, target);
// { appId: "studio", operation: "annotations:dispatch",
// params: { target, intent, docTitle, slug, annotations, formatted },
// source: "studio-ui" }
const result = receiptToCapabilityResult(receipt);
// { ok: true, summary: "Delivered to … via Scout (message).", data: { … } }
```
## Wiring a consumer studio
`AnnotatableDoc` takes an optional `sendTargets` prop. With two or more
entries the send-pass modal shows a small target select and the chosen id
rides on `SendPassPayload.target`; absent or a single entry renders exactly
as before. Routing is the consumer's `onSendPass` job:
```tsx
{
await fetch("/api/scout/messages", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
body: payload.formatted,
target: payload.target ? { agent: payload.target } : undefined,
}),
});
}}
/>
```
## Out of scope (for now)
- No Hudson relay or dispatch bus — HUD-011 alignment is contract-only.
- No liveness registry of our own; Scout's `/api/agents` stays the single
source of truth for presence.
---
# Share studies as Claude artifacts
A study is easy to review in Studio itself, if the reviewer has the repo running. Most reviewers don't. Studio can bundle any study into one self-contained page and publish it as a [Claude artifact](https://claude.ai): a private link you can share like a document. Comments left on the artifact come back into Studio as feedback on the study, and replies written in Studio go back to the artifact thread.
```txt
study (React, in your repo)
→ bun src/artifacts/cli.ts build one HTML page: React, the study, CSS, fonts
→ agent publishes it Artifact tool → claude.ai/artifact/…
→ reviewers comment on the artifact
→ agent imports the comments feedback on the study, in Studio
→ you reply in Studio
→ agent posts the reply back the same artifact thread
```
Studio's host never talks to claude.ai. Only an agent session holds the Artifact tools, so the host keeps the bookkeeping and hands the agent a plan to carry out.
## Choose the studies to share
List them in `apps/studio/src/studio/artifactStudies.ts`:
```ts
export const artifactStudies: readonly ArtifactStudy[] = [
{
id: "talkie-one-thought",
entry: "apps/studio/src/studio/studies/TalkieOneThought.tsx",
export: "TalkieOneThoughtStudy",
},
];
```
| Field | Meaning |
| --- | --- |
| `id` | Artifact slug, and the registry page id unless `page` says otherwise. |
| `entry` | The study module, relative to the repo root. |
| `export` | Named export, rendered with `{ page }`. |
| `page` | Registry page id or href to pass as `page`. |
| `css` | `"app"` (default) brings the app's `globals.css` and fonts; `"own"` relies on the study's CSS imports. |
## Build the pages
```sh
bun src/artifacts/cli.ts build talkie-one-thought
bun src/artifacts/cli.ts build --all
```
Each build writes `.studio/artifacts/.html` and a sidecar `.json` holding a hash of the study's sources. The page carries React, the study, the compiled Tailwind CSS and its fonts inline, so it renders with no network access.
Studies in other Studio-style apps build the same way. Point the CLI at the repo and app:
```sh
bun src/artifacts/cli.ts build --repo ~/dev/fab --app design/studio \
--entry design/studio/src/studio/pages/TuckPage.tsx --export TuckPage \
--page /studio/surfaces/tuck --id tuck
```
## Sync with an agent
Connect your agent to the Studio host's MCP server. `studio mcp` prints the URL and the `claude mcp add` line. Then ask the agent to sync the artifacts. It works through four tools:
| Tool | What it does |
| --- | --- |
| `artifact_sync_plan` | Lists each built study as `publish`, `republish` or `current`, with the comments to read and the Studio replies to post. |
| `artifact_link` | Records a publish: the artifact URL and the source hash that was published. The first link creates the study's feedback page. |
| `artifact_import_comments` | Copies artifact comments into Studio feedback on the study. |
| `artifact_mark_mirrored` | Records that a Studio reply was posted to its artifact thread. |
The agent carries out the plan with its own Artifact and ArtifactComments tools, then reports back through these four.
A study is due for `republish` when its sources change after the last publish. If the plan marks a study `build_stale`, its sources changed since the last build. Rebuild with `build --all` and ask for the plan again.
## How comments travel
- **Into Studio.** Every artifact comment lands as feedback on the study, threaded the way it was on the artifact. Comments already imported are skipped. So are Claude's own replies, including ones that mirrored a Studio reply.
- **Back to the artifact.** Replies written in Studio under an imported thread are posted to that artifact thread, with the reviewer's name as a prefix. A reply can only land on a thread someone with edit access has sent to Claude. Until then it waits in the plan for the next sync.
- **Top-level feedback stays in Studio.** Claude can reply to a thread on an artifact but can't open one. The plan lists those notes as unmirrorable so the agent can tell you, rather than dropping them.
## What stays private
Artifacts are private to the person who publishes them until they share the link. Studio stores links and comment bookkeeping in `.studio/artifacts/links.json`, inside the gitignored `.studio/artifacts/`. Nothing about a published artifact is committed to the repo.
---
# Cloud deploy
> Shipping design studios to a VM's multi-studio host? See
> `docs/cloud-studios.md` — this document covers the platform app deploy
> (`studio.service`, releases under `~/studio-cloud/`).
Studio runs as one application server on an exe.dev VM, reachable only over
tailscale. The VM is the compute host: it installs dependencies, builds the
Next app, and serves the current release. The local machine only ships
sources over SSH.
## Layout on the VM
```
~/studio-cloud//studio/ this repository
~/studio-cloud//hudson/…/hudsonkit/ workspace dependency (dist included)
~/studio-cloud//lattices/design/studio/ workspace dependency
~/studio-cloud//action/design/studio/ manifest only (not imported by the build)
~/studio-cloud/current -> atomic flip target
```
A `studio.service` systemd unit runs `next start` from
`current/studio/apps/studio`, bound to the VM's tailnet IPv4 on port 43210.
## Commands
```sh
studio deploy # ship, install, build, flip, restart, health check
studio deploy --skip-build # update sources in the current built release, restart
studio deploy --host --port