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.
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.
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: Design mode · Chat organization