.md file to compare - side-by-side diff against openui-lang
openui-lang
What it does for you
The format your assistant uses to build live screens for you.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 2/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/openui-lang/SKILL.md
present
state/lib/openui-lang.ts
not present
state/bin/openui-lang/
not present
state/skills/openui-lang/AGENTS.md
present
how it's graded - what counts as a good run 3 criteria · 2 deterministic · 1 judge
Each row is one thing a good run has to get right. deterministic means a quick check decides, pass or fail. judge means the AI reads the result and rates it. Grading each piece on its own (instead of one overall score) shows exactly where a run fell short, so the fix is obvious.
how it runs - the shared frame every skill uses 2/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
This skill doesn't fix its own gaps yet.
state/log/evals.ndjson - compose_inline / Lang for operational/admin/status dashboards, data views, mirror lists, schedules, and structured replies
- HTMLPreview (via compose_inline) for visual / brand-faithful / pixel-perfect / gallery / demo / media / website-like surfaces
- FollowUpBlock for next-step chips
- OpenArtifact when a concrete saved surface already exists
what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
how the work flows- step by step
SKILL.md- the skill, written out in plain English
openui-lang - First-Class Composition Contract
Purpose
OpenUI Lang is the assignment-based, line-oriented DSL used by Snappy OS's OpenUI renderer. The current product contract is larger than the syntax: structured UI is live-composed by the Snappy harness, rendered by snappy-os, and persisted as artifact lang_body so it can be reopened, refined, and versioned as a Live App.
The 2026-05-11 cutover retired the old file-backed surface model. The normal path is no longer "emit a [[TOOL:Lang]] marker" or "write resources/ui.openui." The normal path is:
user intent -> Snappy harness -> compose_inline / Lang -> OpenUI Lang body
-> artifact store lang_body -> OpenArtifact / Live Apps
This skill exists because stale UI doctrine is expensive. If a future agent reintroduces per-shape .tsx files, server regexes, marker emission, or per-skill .openui dashboards as the default path, it is rebuilding retired machinery instead of using the platform.
When to Use This Skill
Use this skill when working on the OpenUI platform contract itself:
- Any prompt mentioning
openui-lang,OpenUI Lang,Lang,
compose_inline, artifact lang_body, renderer behavior, component definitions, prompt generation, or UI-contract audits
- The prompt artifacts in
state/config/openui-lang-prompt*.txt - The Snappy harness tools that compose, open, and refine UI:
compose_inline, OpenArtifact, QueryState, Bash, and the deferred component-catalog lookup path when available. Lang remains the internal renderer event name, not the primary model-facing composition tool.
state/config/openui-lang-prompt.txt(the auto-generated component
signature catalog injected at run time)
- The
Langentry inweb/src/dispatch-card.tsxDISPATCH_REGISTRY web/src/genui-library.tsx(re-exportsopenuiChatLibraryas
genuiLibrary)
- Artifact
lang_bodypersistence and Live Apps reopening behavior - Adding a new generative-UI affordance - primitives-first, not
per-shape file, regex matcher, or retired .openui resource
Do not use this skill as the main executor for ordinary product asks like "build a Live App", "show a FreshBooks dashboard", "make a LinkedIn preview", "create an image gallery", or "make this pixel perfect." Those are concrete surface requests. The agent should ground the relevant data and call compose_inline or exact Lang so the user sees the UI. For reusable app capabilities, app-builder / openui-app owns the lifecycle; this skill only supplies syntax and platform constraints.
Rubric
criteria:
- name: reference_completeness
kind: deterministic
check: "All critical sections present: Current Contract, What is OpenUI Lang, Statement Types, Language Primitives, Operators, @Builtins, Reactive State, Data Fetching, Worked Example, Anti-Patterns, Where it Lives."
- name: example_correctness
kind: deterministic
check: "The worked example (Query + $variable + @Count + Card) parses correctly and demonstrates reactive data binding without hardcoded values."
- name: agent_loader_effective
kind: judge
check: "When a future agent mentions 'Lang' or 'generative UI', they receive the same live-composition + artifact-persistence model from SKILL.md and AGENTS.md and do not reach for markers, per-shape .tsx files, or resources/ui.openui."
What is OpenUI Lang
Plain-English summary: a compact text DSL that says "this UI is built from these named pieces, which compose like this." Each line is one assignment of the form identifier = Expression. The root assignment is special - it's the entry point. Everything else is forward-resolvable: references work both up and down the file.
Why the LLM emits it instead of JSX or JSON:
- Streaming-friendly. The parser validates line-by-line, so partial
output renders incrementally as tokens arrive. JSON's strict bracket pairing and JSX's nesting both stall progressive rendering.
- Token-efficient. Documented as ~67% fewer tokens than the
equivalent JSON tree. On a Card-with-chart-and-table dispatch this is the difference between a 1.2s and a 4s first-paint.
- Graceful invalid-output handling. A malformed sub-expression is
dropped; the rest of the tree still renders. JSON would fail entirely.
- Positional-only.
Card([children], "subtle")not
Card(items: [...], variant: "subtle"). Less syntax for the model to hallucinate; the prop order in defineComponent({props: z.object({...})}) IS the call order.
Conceptual model (four pieces work together):
- Library - Zod schemas + React renderers (in our codebase:
openuiChatLibrary from @openuidev/react-ui, re-exported as genuiLibrary in web/src/genui-library.tsx).
- Prompt Generator - converts the library into the system-prompt
text the LLM reads. Emitted to state/config/openui-lang-prompt.txt and loaded by state/bin/head-screen/server.ts at startup.
- Parser - validates streaming Lang text into typed elements.
- Renderer -
<Renderer library={genuiLibrary}>from
@openuidev/react-lang; maps parsed elements to React components.
The separation lets the LLM focus on what to render while the framework handles how.
Statement Types
Three statement shapes total. Memorize this:
# 1. Component (the common case)
welcomeCard = Card([CardHeader("Welcome"), TextContent("Hello")])
# 2. State (reactive variable)
$days = 7
# 3. Data (Query / Mutation binding)
agents = Query("get_agents", {}, {agents: []}, 30)
Root rule: every program must define root = Card(...) (or any top-level component). That assignment is the entry point.
Forward references work. You can use a name before it's defined; the parser resolves all references after the full input is parsed. This is the "hoisting" rule from state/config/openui-lang-prompt.txt:148.
Reference-or-die rule. Every variable except root MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.
Language Primitives
The compositional building blocks. One-line each. Find the full list in state/config/openui-lang-prompt.txt (auto-generated from the library).
Layout
Card([children], variant?)- outer container. Variants: default, sunk, subtle, raised.CardHeader(title?, subtitle?)- title row inside a Card.Stack([children], direction?, gap?)- flex layout. Direction: row|column. Gap: s|m|l.Col([children], gap?)/Row([children], gap?)- explicit vertical/horizontal stacks.Separator(orientation?, decorative?)- divider.
Lists
ListBlock([ListItem...])- bulleted/numbered runs.ListItem(text, secondary?)Steps([StepsItem...], current?)- ordered phase markers.StepsItem(label, description?, status?)- status: complete|active|pending.Accordion([AccordionItem...])/AccordionItem(title, body)Tabs([TabItem...])/TabItem(label, [body])
Content
TextContent(text, size?)- sizes: small | default | large | small-heavy | large-heavy. Supports markdown.MarkDownRenderer(textMarkdown, variant?)- full GFM markdown.Callout(variant, title, description, visible?)- info|warning|error|success|neutral.TextCallout(variant?, title?, description?)- lighter inline callout.CodeBlock(language, codeString)- syntax-highlighted code.Image(alt, src?)/ImageBlock(src, alt?)/ImageGallery([{src,alt,details}])- fidelity caveat:ImageGalleryis for operational/admin thumbnail lists (settings pages, saved-image pickers). For brand-faithful gallery surfaces (user-facing gallery generation), useHTMLPreviewwith custom HTML instead. Seestate/config/prompt-fragments/image-gallery.md.
Tables
Table([Col...])- column-oriented (each Col holds its own data array).Col(label, data, type?)- type: string|number|action.
Charts (positional series shape)
BarChart(labels[], series[], variant?, xLabel?, yLabel?)- grouped|stacked.LineChart(labels[], series[], variant?, xLabel?, yLabel?)- linear|natural|step.AreaChart(labels[], series[], variant?, xLabel?, yLabel?).PieChart(labels[], series[])/RadarChart(labels[], series[]).RadialChart(labels[], series[])/ScatterChart(labels[], series[]).HorizontalBarChart(labels[], series[], variant?, xLabel?, yLabel?).Series(category, values[])- one data series.
Tags / Buttons / Inputs
Tag(label, variant?)/TagBlock([Tag...]).Button(label, action?)/Buttons([Button...])-actionis an Action expression.Input(label, $binding<string>)/TextArea(label, $binding<string>).Select(label, options[], $binding)/RadioGroup/CheckBoxGroup/Slider/DatePicker.Form([FormControl...])/FormControl(label, [child]).
The LLM has the full ~80 primitive vocabulary at run time. Find every registered name with:
grep -oE '"[A-Z][A-Za-z]+"' \
~/projects/snappy-os-app/apps/snappy-os/web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.js \
| sort -u
Or look up the d.ts at web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.d.ts.
Operators
Arithmetic: +, -, *, /, % Comparison: ==, !=, >, <, >=, <= Logical: &&, || Unary: !, - Ternary: cond ? a : b
@Builtins (closed allowlist)
Functions prefixed with @. Enforced by state/lint/builtin-whitelist.ts
- invented operators silently fail to render. **Do not extend this list
in prose** - it must round-trip with the lint allowlist.
Aggregation
@Count(array)- number of elements.@Sum(numbers[]),@Avg(numbers[]),@Min(numbers[]),@Max(numbers[]).
Array
@First(array),@Last(array)- pluck endpoints.@Filter(array, field, op, value)-opis a comparison operator string.@Sort(array, field, direction?)- direction: asc|desc.@Each(array, varName, template)- iterate; bind each row tovarName.
Math
@Round(number, decimals?),@Abs(number),@Floor(number),@Ceil(number).
Actions (mutation triggers; only inside Action expressions)
@Run(ref)- fire a Mutation by name.@Set($var, value)/@Reset($var, $var2, ...)- write reactive state.@ToAssistant(msg)- send a follow-up user message.@OpenUrl(url)- navigate.
Reactive State and Bindings
Declare state with $name = default:
$days = 7
agents = Query("get_agents", {days: $days}, {agents: []}, 30)
Passing $days to a Slider or Input creates two-way binding - user input updates $days, Query re-fetches automatically. Args marked $binding<type> in primitive signatures accept a $variable reference.
Data Fetching
Query - runs on load, refetches when $variable args change:
data = Query("tool_name", {arg: value}, {default_shape: []}, 30)
Four positional args: tool name, arg object, default shape (rendered while loading), cache TTL in seconds.
Mutation - declared but only fires via @Run():
saveResult = Mutation("save_thread", {title: $title})
Trigger: Button("Save", Action([@Run(saveResult)])).
Member access (dot-pluck):
agents = Query("get_agents", {}, {agents: []}, 30)
names = agents.agents.name # plucks .name from every row → array
count = @Count(agents.agents)
System-Prompt Patterns (canonical)
The OpenUI docs ship a getSystemPrompt(library, options) helper that produces a complete system prompt from a registered library. Our implementation does the equivalent at startup - state/config/openui-lang-prompt.txt is the auto-generated artifact.
Feature flags (set on the prompt generator):
| Flag | Enables | Default |
|---|---|---|
toolCalls | Query(), Mutation(), @Run | true if tools provided |
bindings | $variables, @Set, @Reset | true if toolCalls enabled |
editMode | Incremental edit (patches only) | false |
inlineMode | Text + fenced code responses | false |
Customizable additions: preamble (project context), additionalRules (domain-specific), toolExamples (worked Query/Mutation examples).
In Snappy OS, the prompt artifacts are generated once and then used by the Snappy harness during UI turns. The current doctrine is:
- Default to native
compose_inlinefor dashboards, galleries, previews,
boards, schedules, and structured replies.
- Use
Langdirectly only when the exact authored OpenUI body matters. - Persist reusable generated UI as artifact
lang_body. - Reopen or refine durable UI through
OpenArtifact/ the active-surface
loop, not by reading skill-owned .openui files.
- Ground counts, queues, schedules, artifacts, and metrics with real tool
results (QueryState, Query, or domain tools) before composing.
Variants - Where the Library Choice Matters
Different ecosystems publish different openui*Library exports. Pick by target surface:
| Variant | When to lean on it | Library export |
|---|---|---|
| snappy-os | Chat and Live Apps surface using OpenUI Lang for state/control and typed React renderers for semantic DOM / HTML targets | openuiChatLibrary plus narrow Snappy component groups |
| vercel-ai-chat | Streaming chat with multi-step tool calling via @ai-sdk/openai and useChat - transport-agnostic, <Renderer /> stays unchanged | openuiChatLibrary |
| dashboard | KPI cards, tables, charts driven by MCP-style tool registry; LLM composes UIs that themselves call backend tools | openuiChatLibrary + tool registry |
| react-email | Static, non-interactive content with inline-style requirement (email clients strip <style>) - 44 components in emailLibrary | emailLibrary (from @openuidev/react-email) |
| react-native | Native mobile via Expo - twin libraries (real renderers on device, null renderers for backend prompt generation, since CLI can't import RN in Node) | openuiNativeLibrary |
snappy-os uses the snappy-os variant: openuiChatLibrary from @openuidev/react-ui plus a narrow set of Snappy domain components, re-exported as genuiLibrary / canvasLibrary. Do not bulk-spread openuiLibrary or register Shadcn* generated adapters into the model-visible catalog. The harness calls compose_inline / Lang, the client renders the resulting Lang with <Renderer library={genuiLibrary}>, and durable surfaces persist as artifact lang_body.
For dashboard-like surfaces, still use KPI tiles, Query-driven tables, and charts. The lifecycle changed: dashboards are composed and saved as Live Apps / artifacts, not authored as per-skill resources/ui.openui files.
Worked Example - Query + $variable + @Count + Card
This is the canonical pattern the LLM should produce when asked "how many agents have run?":
$days = 7
agents = Query("get_agents", {days: $days}, {agents: []}, 30)
filtered = @Filter(agents.agents, "lastRun", "!=", null)
$total = "" + @Count(filtered)
root = Card([
CardHeader("Agents", "Last " + $days + " days"),
Stack([
Card([TextContent("Active", "small"), TextContent($total, "large-heavy")], "subtle"),
Slider("Window (days)", $days, 1, 30, 1)
], "row", "m")
])
What this demonstrates:
Queryfor live data (NEVER literal counts)$variablefor derived display values@Filter+@Countto aggregate without server round-trip- String coercion for KPI text:
"" + @Count(...) - Reactive: dragging the Slider updates
$days→ Query refetches → Count
updates
Anti-Patterns
These hard-block. Each has a lint or has cost a session of debugging.
- No raw HTML / JSX strings. Lang is the only path. The previous
KNOWN_COMPONENTS regex catalog in server.ts is being deprecated precisely because it bypassed Lang.
- No
name=valueorname: valuearg syntax. Args are POSITIONAL
ONLY. Colon syntax silently breaks the parser. Burned 2026-04-29T07:42:54Z.
- No invented
@Operators. Whitelist enforced by
state/lint/builtin-whitelist.ts. Adding @Mean to prose without also adding it to the lint allowlist = silent render failure.
- No string concatenation hacks for literal numbers. When the user
asks for a count, USE Query() + @Count(). Hardcoded "47 dispatches" is an LLM fabrication and the DATA-DRIVEN COMPOSITION rule explicitly forbids it.
- No defining variables you don't reference. Unreferenced ≠
rendered. Always thread defined variables back through the parent's children array.
- No per-shape
.tsxfile as the default path for new UI. The
legacy recipe (per-shape file + DISPATCH_REGISTRY entry + server.ts matcher + welcome chip) is dead. Default = describe what you want in a prompt, let the LLM compose primitives.
- No
[[TOOL:Lang]]marker emission as the current contract. That was
the retired inline transport. Use native harness tools (compose_inline, Lang, OpenArtifact) so the run ledger, active surface, and artifact persistence stay coherent.
- No per-skill
.openuidashboards as the normal persistence layer.
state/skills/<slug>/resources/ui.openui and resources/*.openui are retired for new reusable surfaces. Persist reusable UI as artifact lang_body and reopen it through OpenArtifact / Live Apps.
- No
getSystemPrompt()direct calls in a per-request hot path.
Generated once at startup and cached in state/config/openui-lang-prompt.txt.
Where it Lives in Snappy OS
These layers own the current Lang surface end-to-end:
- Prompt artifacts -
state/config/openui-lang-prompt.txtand
state/config/openui-lang-prompt.channel-compose.txt
- Generated by
apps/snappy-os/web/scripts/build-prompt.mts - Describe component signatures, component groups, and usage notes
- Loaded by the Snappy harness / head-screen dispatch path for UI turns
- Harness tools -
compose_inline,OpenArtifact,QueryState,
Bash, plus deferred component-catalog lookup when available.
- Own the runtime transition from natural intent to structured surface
- Preserve active-surface context for follow-ups like "better" and "again"
- Persist reusable output as artifact
lang_body
- Client renderer -
web/src/dispatch-card.tsx,
web/src/right-panel.tsx, web/src/components/artifacts-view.tsx
langDispatchEntry()wraps<Renderer library={genuiLibrary}>- Right panel and Live Apps render saved artifact
lang_body - Five "promoted canned shapes" (DispatchCard, AgentDetail,
ProgressList, PhaseDisclosure, HTMLPreview) also route through Lang
- Threaded shape memory + patch merging in
lang-history.ts
- Library -
web/src/genui-library.tsx
genuiLibrary = openuiChatLibrary(from@openuidev/react-ui)- 5 promoted shapes registered alongside via
defineComponent - Three-line re-export pattern for promoted-shape files (TS
isolatedModules constraint): export { View, Component }, export type { Props }, import { Component } (local binding for the components array)
Resource Files
This skill is prose-only; companion files live in this folder:
AGENTS.md- the per-turn loader. Critical Rules + commands.prompt-fragment.md- compact reminder for the current live-composition
contract.
resources/v05-quickref.md- syntax and component reference. Some older
resource-file audit notes inside it are historical; the lifecycle contract in AGENTS.md and this SKILL.md wins.
Related Skills
- crayon-sdk - the broader Crayon/C1/OpenUI ecosystem
reference. OpenUI Lang is the language layer; crayon-sdk covers the outer chat shell, SSE wire format, thread persistence.
- openui-mcp - Context7 MCP for live OpenUI docs lookup
during work.
- head-screen - the server that injects the Lang
prompt and runs the DISPATCH_REGISTRY Lang entry.
- ui-components - the (now-legacy) per-shape
authoring guide. Read for historical context only; do NOT extend.
Skill Status: ACTIVE Eval: auto-shape (frontmatter + body presence; failure mode is silent: agent doesn't load Lang model and writes per-shape .tsx files) Reference Implementation: snappy-os (web/src/dispatch-card.tsx Lang entry + state/config/openui-lang-prompt.txt)
AGENTS.md- what the AI loads when this skill comes up
openui-lang - loader
Per-turn reference for the current OpenUI contract. Full skill: state/skills/openui-lang/SKILL.md.
This loader is the source of truth for how UI is generated now, after the 2026-05-11 live-composition cutover.
Recurring traps (read before composing brand-row surfaces)
ListBlock(@Each(rows.rows, "r", BrandRow(...)))renders blank dots.ListBlock.itemsisz.array(ListItem.ref); any non-ListItem child gets coerced to{title, subtitle}and the brand row's props are silently dropped. UseStack(@Each(...))instead. The snappy-os server-side rewriter atstate/lib/rewrite-mirror-rows.tsauto-fixes this on everycompose_inlineemission - you do not need to fix it in prose, and prompt-level swaps get reverted by autoformatters anyway. Just emit; the rewriter handles it.- Stack IS available in the Snappy OS
genuiLibraryeven though the upstreamopenuiChatLibraryomits it. Snappy OS registers a narrow local Stack renderer inapps/snappy-os/web/src/genui-library.tsx; do not reintroduce the fullopenuiLibrarybulk import just to recover Stack. - Brand-row click-into-detail. Query-bound rows (GmailRow, etc.) carry an
idas the last positional arg and dispatchsnappy:open-mirror-detailon click - App.tsx handles the rest. Seeui-componentsskill § "Brand-row click-into-detail" for the contract. Do NOT wire row clicks through chat-inject-push. - Static connector binding tables are retired. Connector rows come from Printing Press/local mirror/provider handles discovered at runtime.
state/lint/mirror-recipe-coverage.tsfails if static mirror providers or baked binding tables are registered. Adding connector data means exposing a registeredQuery()/Mutation()provider handle, not adding a baked table.
Activation Boundary
This is a reference loader, not the executor for ordinary product asks.
- If the user asks to **build, show, create, render, redesign, theme, or
refine** a concrete dashboard, preview, gallery, workbench, or Live App, do not answer with OpenUI doctrine. Ground the domain state and call compose_inline (or exact Lang) so the user sees the surface.
- If the user asks for a reusable app capability, stable app identity, saved
Live App, or reopen/refine workflow, treat app-builder / openui-app as the owning skill and use this loader only for syntax constraints.
- Use this loader as the main answer only for platform questions: OpenUI Lang
syntax, renderer behavior, component-library authoring, prompt generation, artifact persistence rules, or audits of the UI contract itself.
Critical Rules
- Concrete UI asks must render. When the user asks for a visual result,
the deliverable is a visible surface. Do not stream this reference, explain the contract, or list guardrails unless the user explicitly asks for documentation. Compose first; explain only if needed after.
- UI is live-composed. The primary path is the native harness tool
compose_inline. It produces OpenUI Lang at runtime from the current intent plus grounded tool results.
- Native tool calls only. Never emit
[[TOOL:Lang]]...[[/TOOL]]
markers. Never rely on inline marker parsing. Use the current callable tools: compose_inline for UI, OpenArtifact for saved artifacts, and QueryState or Bash for grounding. Lang is an internal renderer event, not the model-facing composition tool.
- No per-skill UI files. Do not teach or expect
state/skills/<slug>/resources/ui.openui, resources/*.openui, or skill-owned defineComponent exports. The constitution retired that model. Reusable UI now lives as persisted artifacts with lang_body.
- Artifact store is the persistence layer. A composed surface can be
saved, reopened, and refined because its lang_body is persisted in the artifact store. Reopen durable UI via OpenArtifact, not by reading a skill-owned .openui file.
- Use
Langonly when you need exact authored Lang. Default to
compose_inline for structured UI. Reach for Lang directly when you need to hand-author or patch the exact OpenUI body yourself.
- Positional, root-first Lang. If you emit Lang directly: positional
args only, root = Card(...) or root = Stack(...) first, forward refs allowed, no JSX, no name: value syntax.
- Ground data before you compose. Use
QueryState,Query, or other
real tool results for counts, queue state, artifacts, schedules, and metrics. Do not hardcode numbers or fabricate rows.
- Use
ListComponentsbefore non-obvious components. The component
catalog is lazy by design. For anything beyond the always-on essentials, inspect the actual signature first instead of guessing.
- Render-location claims must match reality. Do not say a surface is
"open on the right" unless the runtime actually mounted an active surface or reopened a saved artifact. If the result is inline, describe it as inline. If the runtime decides the location, do not overclaim.
HTMLPreviewis the high-fidelity renderer for new/rich/visual/brand-faithful/pixel-perfect/demo/gallery/media/website-like surfaces. Use it when the surface is a custom designed artifact, not a generic-component dashboard. For visual image galleries, wrap insidecompose_inlinewith Lang state (url_1..Nvariables) androot = HTMLPreview(html)so the artifact persists, follow-up prompts mutate the SAME surface, and Save Gallery / Publish flows attach. Do NOT use HTMLPreview for operational/admin/status surfaces (System pages, Loop Health, Schedules, Settings) - those use primitives.
compose_inline/Langfor operational/admin/status dashboards, data views, mirror lists, schedules, and structured repliesHTMLPreview(viacompose_inline) for visual / brand-faithful / pixel-perfect / gallery / demo / media / website-like surfacesFollowUpBlockfor next-step chipsOpenArtifactwhen a concrete saved surface already exists
- Refinement means improve the existing surface. For prompts like
"better", "again", "polish", "make it world class", preserve the useful data and improve composition, density, preview fidelity, or placement. Do not reroll an unrelated surface.
Routing
- Use
openui-inline-uiwhen the user needs a one-shot structured reply in
the current conversation.
- Use
openui-appwhen the user wants a reusable live app, saved surface, or
durable cockpit view they can reopen and refine later.
- Use
app-builderwhen the user explicitly wants to create a new reusable
app capability, not just see one answer.
All three still obey the same live-composition + artifact-persistence model. The difference is lifecycle, not transport.
Commands
This loader is reference-only. The current runtime contract is expressed through harness tools, not loader-local scripts.
| purpose | tool / surface |
|---|---|
| compose structured UI | compose_inline |
| hand-author exact Lang | Lang |
| render HTML page | HTMLPreview |
| add next-step chips | FollowUpBlock |
| inspect grounded state | QueryState |
| inspect component signatures | ListComponents |
| reopen saved artifact / live app | OpenArtifact |
Reference docs:
- Full skill:
state/skills/openui-lang/SKILL.md - Constitution:
state/CONSTITUTION.md - Prompt artifact:
state/config/openui-lang-prompt.txt - Renderer:
web/src/dispatch-card.tsx - Libraries:
web/src/genui-library.tsx,web/src/right-panel.tsx
Self-Test
- [ ] I know the default UI path is
compose_inline, not marker emission. - [ ] I will not reference
resources/ui.openuior per-skill.openuifiles. - [ ] I know saved UI persists as artifact
lang_body. - [ ] I will use native tool calls, not
[[TOOL:Lang]]. - [ ] If I emit Lang directly, I will make it positional and root-first.
- [ ] I will ground numbers and queue state through tools before composing UI.
- [ ] I will check component signatures with
ListComponentsbefore guessing. - [ ] I will not claim a surface is on the right unless the runtime actually mounted it there.
Found a gap? Edit this file. <!-- footer-injection-point -->
From snappy-os/CLAUDE.md: Component primitives (the 12 to reach for first)
The 12 most-used OpenUI primitives the LLM will reach for first:
| Primitive | What it's for |
|---|---|
Card / CardHeader | Outer container + title row for any structured response |
Stack / Col | Vertical and horizontal flex layouts (replaces 90% of the old shapes) |
ListBlock / ListItem | Bulleted or numbered runs of items |
Tabs / TabItem | Tabbed content panels |
Accordion / AccordionItem | Expand/collapse rows |
Steps / StepsItem | Ordered phase markers (status timelines) |
BarChart / LineChart / AreaChart / PieChart | Charts with the OpenUI data shape |
Tag / TagBlock | Inline labels and tag groups |
Button / Buttons | Inline action buttons (single or grouped) |
Callout / TextCallout | Inline highlight or warning blocks |
Table | Tabular data |
MarkDownRenderer | Long-form prose with markdown features |
Find the rest:
grep -oE '"[A-Z][A-Za-z]+"' web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.js | sort -u
Or look up types and signatures in web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.d.ts and the per-component .d.ts files under web/node_modules/@openuidev/react-ui/dist/components/.
Provenance: moved from ~/projects/snappy-os-app/apps/snappy-os/CLAUDE.md 2026-05-14.
api.ts- the code it can call
⚠ no api.ts - this skill has no typed action surface
scripts- helper scripts it can run
prose-only skill - 9 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
no recent runs logged - the eval contract is declared but nothing has been graded yet