What's New in 1.0
Custom functions and actions, actions without a wrapper, library.extend, and the message format.
OpenUI Lang 1.0 adds ways for a library to grow (custom functions, custom actions, library.extend) and a message format for storing responses. It also makes actions shorter to write.
Custom functions
A library can add its own @ functions with defineFunction. The model calls them like the built-ins, and the client runs them.
const percent = defineFunction({
name: "Percent",
description: "Formats part / total as a percentage",
params: z.object({ part: z.number(), total: z.number().default(100) }),
returns: z.string(),
fn: ({ part, total }) => `${((part / total) * 100).toFixed(1)}%`,
});
const library = createLibrary({ root: "Card", components, functions: [percent] });share = TextContent("Share: " + @Percent(done, total))Arguments are checked like component props, and a failed call evaluates to null instead of breaking the page. The built-in functions are now defined the same way, so their prompt lines carry types, for example @Sum(numbers: number[]) → number. See Functions.
Custom actions
A library can add its own actions with defineAction. The model uses them like @OpenUrl, and your onAction handler receives them with typed params.
copyBtn = Button("Copy", @CopyToClipboard(order.id))<Renderer
library={library}
response={response}
onAction={(event) => {
if (event.type === "CopyToClipboard") void navigator.clipboard.writeText(event.params.text);
}}
/>AgentInterface takes an onAction prop for the same purpose. See Actions.
One action needs no wrapper
One action no longer needs Action([...]):
askBtn = Button("Tell me more", @ToAssistant("Tell me more about this order"))
saveBtn = Button("Save", Action([@Run(save), @Run(orders), @Reset($note)]))Several actions still go in Action([...]). The prompt teaches both forms. See Actions.
Context for @ToAssistant
The second argument of @ToAssistant can be any value, and the host receives it unchanged in event.params.context:
approveBtn = Button("Approve", @ToAssistant("Approve this order", {orderId: 42}))Action props
action()returns the schema for an action prop:action: action().optional(). It replacestagSchemaId(z.any(), "ActionExpression").- In the JSON Schema, an action prop is
{"$ref": "#/$defs/ActionExpression"}, like a component slot. - A prop can name the actions it takes, for example
z.union([copyToClipboard.ref, steps.OpenUrl.ref]), printed as@CopyToClipboard | @OpenUrl. This shapes the prompt and the JSON Schema only. The runtime does not enforce it yet.
See Actions.
library.extend
Derive a library from an existing one. Add, override, or remove components, and add functions and actions:
const shopLibrary = openuiChatLibrary.extend({
components: {
add: [{ component: ProductCard, slots: ["Card"] }],
override: [CodeBlock],
remove: ["RadarChart"],
},
functions: { add: [formatCurrency] },
actions: { add: [addToCart] },
});The derived library has no id. Create it once at module scope. See Extending a library.
Entry rule
The entry is the statement named root. Without one, the first statement that calls the library's root component is used, with a no-root warning. See Entry Statement.
On the server, pass the root component name to the parser so this fallback works:
const parser = createParser(library.toJSONSchema(), library.root);createStreamingParser takes the same second argument. Without it, a program with no root statement reports no-root and renders nothing.
Message format
buildMessage and parseMessage read and write responses wrapped in ]]>openui: marker lines, with optional context such as form values. The Renderer accepts a stored message as response.
]]>openui:content
root = Card([title])
title = TextContent("Order 42")
]]>openui:context
{"formState":{"note":"rush"}}
]]>openui:endSee Message Protocol.
Vue, Svelte, and Angular
@openuidev/vue-lang, @openuidev/svelte-lang, and @openuidev/angular-lang stay on 0.4. Keep @openuidev/lang-core at ^0.4 with them. The 1.0 language features (actions without a wrapper, custom functions, custom actions) need the React renderer for now.