Documentation source
Asset Studio
Chat-driven creation canvas for all output kinds — views, images, videos, and documents — with entity-scoped context and a unified Files catalog.
## Overview
Asset Studio is the in-app creation surface where you chat with an agent and watch the artifact render live. It generalizes the previous video-only `/studio` route into a side-by-side shell that handles every output kind the platform supports: views (decks, pages, experiences), images, videos, and documents.
The Studio is **presentation chrome over existing primitives**. It adds no new tables, no new render paths, and no new agent tools. Canvas rendering delegates entirely to the same components used everywhere else: `PersistedViewRenderer` for views, `RenderDetail` for videos, `DocumentPreview` for documents, and a plain `<img>` for images.
A companion storage unification rides along: AI-generated assets (images, completed video renders) are now cataloged as `documents` rows with `source='generated'`, so the entity Files panel, `listDocuments`, and the studio artifact rail all read from one catalog instead of a three-way union.
## Key Concepts
### StudioArtifact
The canvas's input type. A discriminated union over all four output kinds:
```typescript
type StudioArtifact =
| { kind: "view"; viewId: string; title?: string; ephemeral?: PublicAuthorView }
| { kind: "image"; url: string; documentId?: string; title?: string }
| { kind: "video"; renderId: string; title?: string }
| { kind: "document"; documentId: string; title?: string };
```
`features/studio/lib/artifact.ts`
### Creation Intents
Six data-literal entries (`CREATION_INTENTS`) that map to the output kinds above. Each carries a `promptSeed` function that contextualizes the starter chat message when a record is in scope. Intents reference only canonical tools — never the deprecated `generateView`/`saveTransientView` pair.
`features/studio/lib/creation-intents.ts`
### StudioArtifactBridge
A React context (`StudioArtifactBridgeContext`) mounted only inside the studio shell. When a completed tool result in the chat panel maps to a `StudioArtifact`, the bridge publishes it to the canvas. The context default is `null`, so every other chat surface in the app is unaffected.
Deduplication is keyed on `toolCallId` at the provider level — card remounts do not re-publish.
`features/studio/lib/studio-bridge.tsx`
### Generated-Asset Cataloging
`documents.source` (`'uploaded' | 'generated'`) and `documents.origin` (JSONB) were added in migration `20260612000000_documents_generated_source.sql`. Writers (`generateAndUploadImage`, video render completion) insert a row post-upload. Catalog writes are best-effort — a failure is captured in Sentry but does not fail the generation.
## How It Works
### Studio Shell Layout
```
/studio?entity=<uuid>&kind=<intent>
↓
app/(app)/studio/page.tsx (server component — validates entity/view access)
↓
features/studio/components/studio-shell.tsx (client — ResizablePanelGroup)
├── ChatPanel (existing component, agentId="studio")
│ └── tool-call-card.tsx ← bridge hook (one line; null-safe)
├── StudioCanvas
│ ├── empty state: CREATION_INTENTS cards
│ └── active artifact: kind-switched renderer
│ view → PersistedViewRenderer / BlockHostClient (ephemeral)
│ image → <img> + download / set-as-record-image
│ video → RenderDetail (unchanged component, new host)
│ document→ DocumentPreview
├── StudioActionBar (publish / pin / present / edit in designer)
└── StudioArtifactRail (horizontal strip — listEntityOutputs one round-trip)
```
Mobile (`useIsMobile()`): segmented Chat | Canvas toggle, `100dvh`, `--fab-clearance` on both panes.
### Artifact Flow
1. User picks a creation intent → `promptSeed` populates chat input.
2. Agent calls a canonical tool (`publish_view`, `generateImage`, `generateVideo`, `listDocuments`).
3. `tool-call-card.tsx` calls `artifactFromToolOutput(result)` → `bridge.publish(artifact)` if non-null.
4. Canvas switches kind, renders the artifact.
5. Action bar offers context-appropriate actions (slides-root view gets "Open presentation" + "Edit in designer").
### list-entity-outputs Server Action
`features/studio/server/list-entity-outputs.ts` — single round trip for the artifact rail:
```typescript
async function listEntityOutputs(entityId: string): Promise<EntityOutputs>
// Returns: { files: DocumentRow[], renders: VideoRenderRow[], pinnedViewId?, deckTemplateViewId? }
// Requires: assertEntityAccess(entityId, "read") — tenant-scoped via getTenantContext()
```
The shared type lives in `features/studio/server/list-entity-outputs-shared.ts` (sibling to the `"use server"` file per the Next 16 async-exports rule).
### publish_view Update Branch
`publish_view` gained an optional `updateViewId` parameter. When provided, it loads the target row (tenant-scoped), verifies `scope === "shared"` or `created_by === caller`, rewrites the definition through the standard normalization path, and bumps `updated_at`. No second write path — same tool, same permission gate.
## API Reference
| Symbol | Location | Purpose |
|---|---|---|
| `listEntityOutputs` | `features/studio/server/list-entity-outputs.ts` | Rail + Files tab data in one call |
| `artifactFromToolOutput` | `features/studio/lib/artifact-from-tool-output.ts` | Maps tool results → StudioArtifact (pure, tested) |
| `CREATION_INTENTS` | `features/studio/lib/creation-intents.ts` | Six intent data literals |
| `useStudioArtifactBridge` | `features/studio/lib/studio-bridge.tsx` | Hook for publishing to canvas |
| `publish_view` (updateViewId) | `features/tools/publish-view-tools.ts` | Iterate on an existing view in place |
| `listDocuments` (source filter) | `features/tools/document-tools.ts` | `source?: 'uploaded' \| 'generated'` |
## For Agents
**Use the `studio` system agent** when a user is in the studio shell. Its system prompt is focused on output creation: read entity context → propose artifact kind → generate via canonical tools → iterate on feedback.
**Canonical tools for creation:**
| Output kind | Tool | Notes |
|---|---|---|
| View (deck / page / experience) | `publish_view` | Pass `updateViewId` to iterate in place |
| Image | `generateImage` / `generateImages` | Returns `documentId` alongside `url` |
| Video | `planVideoStoryboard` → `generateVideo` | `renderId` appears in canvas via bridge |
| Document | `createDocument` / `listDocuments` | |
**NEVER call `generateView` or `saveTransientView`** — both are deprecated (removal target: 2026-07-15). The studio's `CREATION_INTENTS` prompt seeds never reference them.
**Bridge behavior:** Outside the studio shell, `useStudioArtifactBridge()` returns `null`. Tool results behave identically to plain chat — no canvas, no overlay. Do not check for bridge presence in tool implementations.
## Design Decisions
### Chrome-not-engine
The studio adds presentation chrome over existing primitives. This was a hard constraint from the spec: zero new tables for artifacts, zero new render paths, zero new agent tools. The `StudioArtifact` union is a canvas input type, not a storage primitive — it maps to existing rows (`views`, `documents`, `video_renders`) that already exist.
### Documents catalog unification
Generated images and videos were previously orphaned from the `documents` table (images had no row; videos only had `video_renders`). Adding `source` and `origin` columns to `documents` — rather than a new `generated_assets` table — keeps `listDocuments`, the Files panel, and the studio rail reading from one catalog. `video_renders` stays the job record per ADR-0050; the documents row is the catalog entry.
### Replace-means-remove: studio-client.tsx deleted
`features/videos/components/studio-client.tsx` is deleted in this PR. Its layout job is fully absorbed. The underlying components it hosted (`VideoCreateForm`, `RenderDetail`, `StoryboardEditor`) survive and are mounted by the video canvas kind.
### Build-with-AI reroute
The "Build with AI" button in the view designer navigates to `/studio?view=<id>` instead of opening the agent sidebar with a `generateView` prompt. This is the last UX surface seeding the deprecated flow, clearing the path for its deletion (2026-07-15).
### O-PR-3 seam
The studio (in-app chat iteration) and O-PR-3 (public-comment-driven regeneration) are complementary, not duplicated. The bridge publishes tool results inside the studio; O-PR-3's comment loop triggers a new session from outside. They share `publish_view` as the write primitive.
## Related Modules
- [Video Studio](/docs/features/video-studio) — ADR-0050 governs the video render/job boundary; `video_renders` stays the execution record
- [View System](/docs/features/view-system) — `PersistedViewRenderer`, `publish_view`, `attachToEntityId`
- [Document Processing](/docs/features/document-processing) — `documents` table, `source`/`origin` columns, `listDocuments`
- [Agent System](/docs/features/agent-system) — `studio` system agent registration in `system-agents.ts`
- [Tool System](/docs/features/tool-system) — `publish_view`, `generateImage`, `generateVideo`, `listDocuments`