Skip to content
MISSIONBuilding to $50K/year · partnership threshold — every $20 subscription reserves 10 meals in the fund. Report published monthly.SEE THE PROGRESS →
DOC · UPDATED 2026-08-07

Artifacts.

Substantial assistant outputs — code files, long documents, HTML documents, design files — are lifted out of the conversation into a dedicated versioned side panel.

An artifact is anything the assistant produces that is bigger than a paragraph and meant to be kept, edited, shared, or run. LADLE detects them automatically on the server side (during the same request that streamed the reply), writes them to the artifacts + artifact_versions tables, and emits a sentinel event on the response stream so the client can open the panel synchronously. Four types are supported.

The four artifact types

Type is inferred from the reply content by heuristic on the server.

codeAny fenced code block ≥ 40 lines. Language is captured from the fence declaration. Rendered with Shiki syntax highlighting.
htmlA complete HTML document (either <!doctype html>... </html> or <html>... </html> anywhere in the reply). Rendered in a sandboxed iframe.
markdownLong-form prose ≥ 600 words IN RESPONSE TO a user prompt containing a write/draft/make/create verb. Prevents accidental prose replies from being lifted.
designIn a design-mode chat only, any HTML the model emitted (complete or partial). Wraps the same iframe preview with device-width toggles.

When detection runs

On the server, immediately after the assistant message is persisted, before the response stream closes. Detection is deterministic — same input produces same output — and idempotent. If the same reply is processed twice, only one artifact row is created (the second run's insert is racing, and the DB unique constraint on message_id resolves the tie).

The LADLE_ARTIFACT stream sentinel

When an artifact is created, the server enqueues a line into the response stream just before controller.close() in the form `LADLE_ARTIFACT:<json>` where <json> is a single-line JSON payload of { id, version, type, title, language, bodyKey, chatId }. The client parses these out of the stream via extractSearchMarker, populates the artifacts state, and opens the panel synchronously — no separate POST, no race with the router.replace that follows on new-chat creation.

Versioning model

Every artifact has one or more numbered versions. Version 1 is created alongside the artifact row; subsequent versions are appended by appendArtifactVersion(). The panel's version selector browses them.

Row: artifactsid, user_id, chat_id, type, title, language, created_at, updated_at
Row: artifact_versionsartifact_id, version (int, ≥1), content (text), message_id (nullable, points to the assistant message that produced it), created_at
First versionAlways version=1. Created together with the artifact row.
New versionappendArtifactVersion() picks max(version)+1 and inserts. Also bumps artifacts.updated_at.
Latest versionRead via listArtifactsForChat's artifact_versions embed; the summariser picks max(version) client-side.

The artifact panel

Slides in from the right at min(45vw, 720px) on desktop; collapses to full-width on ≤900px. Header shows type + language + title + version selector + Copy + Download. Body renders per-type: code with Shiki, markdown with the app's markdown renderer, html and design with sandboxed iframes. Sandboxed with `allow-scripts allow-forms` only — no `allow-same-origin` — so any script in a design artifact can't reach the parent frame.

RLS and ownership

artifacts and artifact_versions are owner-only. artifacts_select_own = user_id = auth.uid(). artifact_versions_select_own = the parent artifact must be owned by the caller. Anon cannot read. Service role bypasses RLS (used by the send route to write during streaming). RLS is verified 19/19 in the regression suite.

Inline card swap

When an artifact was detected from a code block that also appears inline in the transcript, LADLE hot-swaps that code block for a compact ArtifactCard on render. The mapping is done via bodyKey (first 200 chars of the artifact's content, trimmed) — the client looks up the key in its artifactBodies map and, if present, renders the card instead of the raw fence. Keeps long code out of the reading flow while preserving the click-to-open affordance.

Listing and filtering

GET /api/app/artifacts?chatId=X returns everything for a chat. GET /api/app/artifacts returns everything for the caller (up to 200). The /artifacts page renders the full list with type/language filters, sort (recent / A-Z), search, and a list/grid toggle. The Library page (/library) is Starred messages, not artifacts — they're conceptually distinct.

Docs stay current. Report drift.

If a limit in the product doesn't match the number on this page, that's a bug — help@ladle.chat.

See all docsWatch it work
$20/MO · CANCEL ANYTIME · $0.80/MEAL · RECEIPTS MONTHLY
Read the ledger →
0meals fundedROLLING TOTAL · UPDATED HOURLY
SEE THE RECEIPTS →