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 replaces tagSchemaId(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:end

See 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.

On this page